From adcc0ebf3edf0c0da77bf837a0fc4fc4938ff86a Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 17:52:48 +0100 Subject: [PATCH 01/21] feat(auth): introduce rate limit service interface - Define RateLimitService abstract class with checkRequest and dispose methods - Implement rate limiting logic to prevent abuse of sensitive or expensive endpoints - Use unique key (e.g., IP address) to track and limit requests - Throw ForbiddenException when rate limit is exceeded - Provide flexibility for different rate limiting strategies in implementations --- lib/src/services/rate_limit_service.dart | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 lib/src/services/rate_limit_service.dart diff --git a/lib/src/services/rate_limit_service.dart b/lib/src/services/rate_limit_service.dart new file mode 100644 index 0000000..8a80f3b --- /dev/null +++ b/lib/src/services/rate_limit_service.dart @@ -0,0 +1,37 @@ +import 'package:core/core.dart'; + +/// {@template rate_limit_service} +/// Defines the interface for a service that provides rate-limiting capabilities. +/// +/// This service is used to check and record requests against a specific key +/// (e.g., an IP address or email) to prevent abuse of sensitive or expensive +/// endpoints. +/// {@endtemplate} +abstract class RateLimitService { + /// {@macro rate_limit_service} + const RateLimitService(); + + /// Checks if a request associated with the given [key] is allowed. + /// + /// This method performs the following logic: + /// 1. Counts the number of recent requests for the [key] within the [window]. + /// 2. If the count is greater than or equal to the [limit], it throws a + /// [ForbiddenException] indicating the rate limit has been exceeded. + /// 3. If the count is below the limit, it records the current request + /// (e.g., by storing a timestamp) and allows the request to proceed. + /// + /// - [key]: A unique identifier for the request source (e.g., IP address). + /// - [limit]: The maximum number of requests allowed within the window. + /// - [window]: The time duration to consider for counting requests. + /// + /// Throws [ForbiddenException] if the rate limit is exceeded. + /// Throws [OperationFailedException] for unexpected errors during the check. + Future checkRequest({ + required String key, + required int limit, + required Duration window, + }); + + /// Disposes of any resources used by the service (e.g., timers). + void dispose(); +} From 484d7db7dc20df770545a836547b510e61e6bd52 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 17:53:35 +0100 Subject: [PATCH 02/21] feat(rate-limit): implement MongoDB-backed rate limit service - Add MongoDbRateLimitService class implementing RateLimitService interface - Use MongoDB TTL index for efficient automatic purging of old records - Implement checkRequest method with counting and limiting logic - Add error handling and logging --- .../services/mongodb_rate_limit_service.dart | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 lib/src/services/mongodb_rate_limit_service.dart diff --git a/lib/src/services/mongodb_rate_limit_service.dart b/lib/src/services/mongodb_rate_limit_service.dart new file mode 100644 index 0000000..3817166 --- /dev/null +++ b/lib/src/services/mongodb_rate_limit_service.dart @@ -0,0 +1,87 @@ +import 'package:core/core.dart'; +import 'package:data_mongodb/data_mongodb.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/services/rate_limit_service.dart'; +import 'package:logging/logging.dart'; +import 'package:mongo_dart/mongo_dart.dart'; + +/// The name of the MongoDB collection for storing rate limit attempts. +const String kRateLimitAttemptsCollection = 'rate_limit_attempts'; + +/// {@template mongodb_rate_limit_service} +/// A MongoDB-backed implementation of [RateLimitService]. +/// +/// This service tracks request attempts in a dedicated MongoDB collection. +/// It relies on a TTL (Time-To-Live) index on the `createdAt` field to +/// ensure that old request records are automatically purged by the database, +/// which is highly efficient. +/// {@endtemplate} +class MongoDbRateLimitService implements RateLimitService { + /// {@macro mongodb_rate_limit_service} + MongoDbRateLimitService({ + required MongoDbConnectionManager connectionManager, + required Logger log, + }) : _connectionManager = connectionManager, + _log = log; + + final MongoDbConnectionManager _connectionManager; + final Logger _log; + + DbCollection get _collection => + _connectionManager.db.collection(kRateLimitAttemptsCollection); + + @override + Future checkRequest({ + required String key, + required int limit, + required Duration window, + }) async { + try { + final now = DateTime.now(); + final windowStart = now.subtract(window); + + // 1. Count recent requests for the given key within the time window. + final recentRequestsCount = await _collection.count( + where.eq('key', key).and(where.gte('createdAt', windowStart)), + ); + + _log.finer( + 'Rate limit check for key "$key": Found $recentRequestsCount ' + 'requests in the last ${window.inMinutes} minutes (limit is $limit).', + ); + + // 2. If the limit is reached or exceeded, throw an exception. + if (recentRequestsCount >= limit) { + _log.warning( + 'Rate limit exceeded for key "$key". ' + '($recentRequestsCount >= $limit)', + ); + throw ForbiddenException( + 'You have made too many requests. Please try again later.', + ); + } + + // 3. If the limit is not reached, record the new request. + await _collection.insertOne({ + '_id': ObjectId(), + 'key': key, + 'createdAt': now, + }); + _log.finer('Recorded new request for key "$key".'); + } on HttpException { + // Re-throw exceptions that we've thrown intentionally. + rethrow; + } catch (e, s) { + _log.severe('Error during rate limit check for key "$key"', e, s); + throw OperationFailedException( + 'An unexpected error occurred while checking request rate limits.', + ); + } + } + + @override + void dispose() { + // This is a no-op because the underlying database connection is managed + // by the injected MongoDbConnectionManager, which has its own lifecycle. + _log.finer('dispose() called, no action needed.'); + } +} From 6efac460a31eaf08ffa8c8fed3d4ad2825077675 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 17:55:07 +0100 Subject: [PATCH 03/21] feat(rate-limiting): implement rate limiting service - Add RateLimitService interface - Implement MongoDbRateLimitService - Integrate RateLimitService into AppDependencies - Update dependency initialization and disposal --- lib/src/config/app_dependencies.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index e14d15f..1b7f543 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -14,7 +14,9 @@ import 'package:flutter_news_app_api_server_full_source_code/src/services/databa import 'package:flutter_news_app_api_server_full_source_code/src/services/default_user_preference_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/jwt_auth_token_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_token_blacklist_service.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_rate_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_verification_code_storage_service.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/services/rate_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/token_blacklist_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/user_preference_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/verification_code_storage_service.dart'; @@ -65,6 +67,7 @@ class AppDependencies { late final DashboardSummaryService dashboardSummaryService; late final PermissionService permissionService; late final UserPreferenceLimitService userPreferenceLimitService; + late final RateLimitService rateLimitService; /// Initializes all application dependencies. /// @@ -222,6 +225,10 @@ class AppDependencies { permissionService: permissionService, log: Logger('DefaultUserPreferenceLimitService'), ); + rateLimitService = MongoDbRateLimitService( + connectionManager: _mongoDbConnectionManager, + log: Logger('MongoDbRateLimitService'), + ); _isInitialized = true; _log.info('Application dependencies initialized successfully.'); @@ -238,6 +245,7 @@ class AppDependencies { if (!_isInitialized) return; await _mongoDbConnectionManager.close(); tokenBlacklistService.dispose(); + rateLimitService.dispose(); _isInitialized = false; _log.info('Application dependencies disposed.'); } From 2f82e0930f26a899a3c874c17534af9805e460dd Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 17:56:18 +0100 Subject: [PATCH 04/21] feat(database): add rate limit attempts TTL and key indexes - Add TTL index for automatic document expiration in rate limit attempts collection - Add key index for faster lookups in rate limit attempts collection - Implement indexing in the DatabaseSeedingService --- .../services/database_seeding_service.dart | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/src/services/database_seeding_service.dart b/lib/src/services/database_seeding_service.dart index 7559967..d7a3e51 100644 --- a/lib/src/services/database_seeding_service.dart +++ b/lib/src/services/database_seeding_service.dart @@ -1,5 +1,6 @@ import 'package:core/core.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/config/environment_config.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_rate_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_token_blacklist_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_verification_code_storage_service.dart'; import 'package:logging/logging.dart'; @@ -300,6 +301,25 @@ class DatabaseSeedingService { ], }); + // Index for the rate limit attempts collection + await _db.runCommand({ + 'createIndexes': kRateLimitAttemptsCollection, + 'indexes': [ + { + // This is a TTL index. MongoDB will automatically delete request + // attempt documents 24 hours after they are created. + 'key': {'createdAt': 1}, + 'name': 'createdAt_ttl_index', + 'expireAfterSeconds': 86400, // 24 hours + }, + { + // Index on the key field for faster lookups. + 'key': {'key': 1}, + 'name': 'key_index', + }, + ], + }); + _log.info('Database indexes are set up correctly.'); } on Exception catch (e, s) { _log.severe('Failed to create database indexes.', e, s); From 167af20bffc8cd66b5edc86f35eb74a3aa8f802a Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 17:57:35 +0100 Subject: [PATCH 05/21] feat(middlewares): implement rate limiter middleware - Add rateLimiter middleware function to enforce rate limiting on routes - Include ipKeyExtractor for IP-based rate limiting - Implement _getIpAddress to extract client's IP address from request - Add RateLimitService for tracking and limiting requests --- .../middlewares/rate_limiter_middleware.dart | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 lib/src/middlewares/rate_limiter_middleware.dart diff --git a/lib/src/middlewares/rate_limiter_middleware.dart b/lib/src/middlewares/rate_limiter_middleware.dart new file mode 100644 index 0000000..d7a1730 --- /dev/null +++ b/lib/src/middlewares/rate_limiter_middleware.dart @@ -0,0 +1,65 @@ +import 'package:dart_frog/dart_frog.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/services/rate_limit_service.dart'; + +/// Extracts the client's IP address from the request. +/// +/// It prioritizes the 'X-Forwarded-For' header, which is standard for +/// identifying the originating IP of a client connecting through a proxy +/// or load balancer. If that header is not present, it falls back to the +/// direct connection's IP address. +String? _getIpAddress(RequestContext context) { + final headers = context.request.headers; + // The 'X-Forwarded-For' header can contain a comma-separated list of IPs. + // The first one is typically the original client IP. + final xff = headers['X-Forwarded-For']?.split(',').first.trim(); + if (xff != null && xff.isNotEmpty) { + return xff; + } + // Fallback to the direct connection IP if XFF is not available. + return context.request.connectionInfo?.remoteAddress.address; +} + +/// Middleware to enforce rate limiting on a route. +/// +/// This middleware uses the [RateLimitService] to track and limit the number +/// of requests from a unique source (identified by a key) within a specific +/// time window. +/// +/// - [limit]: The maximum number of requests allowed. +/// - [window]: The time duration in which the requests are counted. +/// - [keyExtractor]: A function that extracts a unique key from the request +/// context. This could be an IP address, an email from the body, etc. +Middleware rateLimiter({ + required int limit, + required Duration window, + required Future Function(RequestContext) keyExtractor, +}) { + return (handler) { + return (context) async { + final rateLimitService = context.read(); + final key = await keyExtractor(context); + + // If a key cannot be extracted, we bypass the rate limiter. + // This is a safeguard; for IP-based limiting, a key should always exist. + if (key == null || key.isEmpty) { + return handler(context); + } + + // The checkRequest method will throw a ForbiddenException if the + // limit is exceeded. This will be caught by the global error handler. + await rateLimitService.checkRequest( + key: key, + limit: limit, + window: window, + ); + + // If the check passes, proceed to the next handler. + return handler(context); + }; + }; +} + +/// A specific implementation of the [keyExtractor] for IP-based rate limiting. +Future ipKeyExtractor(RequestContext context) async { + return _getIpAddress(context); +} From b7e6faa63944f10e798bf268cb668f1c44aaf099 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 17:59:28 +0100 Subject: [PATCH 06/21] fix(error_handler): map rate limiting errors to 429 status code - Add special case for ForbiddenException containing 'too many requests' - Map to 429 Too Many Requests status code for rate limiting errors --- lib/src/middlewares/error_handler.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/middlewares/error_handler.dart b/lib/src/middlewares/error_handler.dart index 6cc5ed7..57c2732 100644 --- a/lib/src/middlewares/error_handler.dart +++ b/lib/src/middlewares/error_handler.dart @@ -66,6 +66,12 @@ Middleware errorHandler() { /// Maps HttpException subtypes to appropriate HTTP status codes. int _mapExceptionToStatusCode(HttpException exception) { + // Special case for rate limiting + if (exception is ForbiddenException && + exception.message.contains('too many requests')) { + return 429; // Too Many Requests + } + return switch (exception) { InvalidInputException() => HttpStatus.badRequest, // 400 AuthenticationException() => HttpStatus.unauthorized, // 401 From c889ba43b554c6b23d04d4cddcc6c6f6c72c4d18 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 17:59:48 +0100 Subject: [PATCH 07/21] feat(routes): add rate limit service to middleware - Import RateLimitService from services package - Add RateLimitService to the middleware chain using provider --- routes/_middleware.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/routes/_middleware.dart b/routes/_middleware.dart index 419731c..98f813a 100644 --- a/routes/_middleware.dart +++ b/routes/_middleware.dart @@ -10,6 +10,7 @@ import 'package:flutter_news_app_api_server_full_source_code/src/registry/model_ import 'package:flutter_news_app_api_server_full_source_code/src/services/auth_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/auth_token_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/dashboard_summary_service.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/services/rate_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/token_blacklist_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/user_preference_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/verification_code_storage_service.dart'; @@ -131,6 +132,7 @@ Handler middleware(Handler handler) { (_) => deps.userPreferenceLimitService, ), ) + .use(provider((_) => deps.rateLimitService)) .call(context); }; }); From d2c8e7e8010db025a4fd9ea455389e50cfce4020 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 18:03:06 +0100 Subject: [PATCH 08/21] feat(auth): add rate limiting to request-code endpoint - Implement rate limiting middleware for the /request-code endpoint - Allow up to 3 requests per IP address every 24 hours - Use ipKeyExtractor for rate limiting key generation - Refactor handler logic to include rate limiting --- routes/api/v1/auth/request-code.dart | 84 ++++++++++++++++------------ 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/routes/api/v1/auth/request-code.dart b/routes/api/v1/auth/request-code.dart index c7b45cc..57b16bd 100644 --- a/routes/api/v1/auth/request-code.dart +++ b/routes/api/v1/auth/request-code.dart @@ -2,28 +2,14 @@ import 'dart:io'; import 'package:core/core.dart'; // For exceptions import 'package:dart_frog/dart_frog.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/rate_limiter_middleware.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/auth_service.dart'; import 'package:logging/logging.dart'; // Create a logger for this file. final _logger = Logger('request_code_handler'); -/// Handles POST requests to `/api/v1/auth/request-code`. -/// -/// Initiates an email-based sign-in process. This endpoint is context-aware. -/// -/// - For the user-facing app, it sends a verification code to the provided -/// email, supporting both sign-in and sign-up. -/// - For the dashboard, the request body must include `"isDashboardLogin": true`. -/// In this mode, it first verifies the user exists and has 'admin' or -/// 'publisher' roles before sending a code, effectively acting as a -/// login-only gate. -Future onRequest(RequestContext context) async { - // Ensure this is a POST request - if (context.request.method != HttpMethod.post) { - return Response(statusCode: HttpStatus.methodNotAllowed); - } - +Future _onRequest(RequestContext context) async { // Read the AuthService provided by middleware final authService = context.read(); @@ -70,25 +56,53 @@ Future onRequest(RequestContext context) async { } try { - // Call the AuthService to handle the logic, passing the context flag. - await authService.initiateEmailSignIn( - email, - isDashboardLogin: isDashboardLogin, - ); + // Call the AuthService to handle the logic, passing the context flag. + await authService.initiateEmailSignIn( + email, + isDashboardLogin: isDashboardLogin, + ); - // Return 202 Accepted: The request is accepted for processing, - // but the processing (email sending) hasn't necessarily completed. - // 200 OK is also acceptable if you consider the API call itself complete. - return Response(statusCode: HttpStatus.accepted); - } on HttpException catch (_) { - // Let the central errorHandler middleware handle known exceptions - rethrow; - } catch (e, s) { - // Catch unexpected errors from the service layer - _logger.severe('Unexpected error in /request-code handler', e, s); - // Let the central errorHandler handle this as a 500 - throw const OperationFailedException( - 'An unexpected error occurred while requesting the sign-in code.', - ); + // Return 202 Accepted: The request is accepted for processing, + // but the processing (email sending) hasn't necessarily completed. + // 200 OK is also acceptable if you consider the API call itself complete. + return Response(statusCode: HttpStatus.accepted); +} on HttpException catch (_) { + // Let the central errorHandler middleware handle known exceptions + rethrow; +} catch (e, s) { + // Catch unexpected errors from the service layer + _logger.severe('Unexpected error in /request-code handler', e, s); + // Let the central errorHandler handle this as a 500 + throw const OperationFailedException( + 'An unexpected error occurred while requesting the sign-in code.', + ); +} +} + +/// Handles POST requests to `/api/v1/auth/request-code`. +/// +/// Initiates an email-based sign-in process. This endpoint is context-aware. +/// +/// - For the user-facing app, it sends a verification code to the provided +/// email, supporting both sign-in and sign-up. +/// - For the dashboard, the request body must include `"isDashboardLogin": true`. +/// In this mode, it first verifies the user exists and has 'admin' or +/// 'publisher' roles before sending a code, effectively acting as a +/// login-only gate. +Future onRequest(RequestContext context) async { + // Ensure this is a POST request + if (context.request.method != HttpMethod.post) { + return Response(statusCode: HttpStatus.methodNotAllowed); } + + // Apply the rate limiter middleware before calling the actual handler. + final handler = const Pipeline().addMiddleware( + rateLimiter( + limit: 3, + window: const Duration(hours: 24), + keyExtractor: ipKeyExtractor, + ), + ).addHandler(_onRequest); + + return handler(context); } From c19ec59fb2a841eb4cc890d9a78c0ba878a7b193 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 18:06:05 +0100 Subject: [PATCH 09/21] lint: misc --- lib/src/config/app_dependencies.dart | 2 +- lib/src/middlewares/rate_limiter_middleware.dart | 4 ++-- lib/src/services/mongodb_rate_limit_service.dart | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index 1b7f543..75dca6b 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -13,8 +13,8 @@ import 'package:flutter_news_app_api_server_full_source_code/src/services/dashbo import 'package:flutter_news_app_api_server_full_source_code/src/services/database_seeding_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/default_user_preference_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/jwt_auth_token_service.dart'; -import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_token_blacklist_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_rate_limit_service.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_token_blacklist_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/mongodb_verification_code_storage_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/rate_limit_service.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/token_blacklist_service.dart'; diff --git a/lib/src/middlewares/rate_limiter_middleware.dart b/lib/src/middlewares/rate_limiter_middleware.dart index d7a1730..4e0dac0 100644 --- a/lib/src/middlewares/rate_limiter_middleware.dart +++ b/lib/src/middlewares/rate_limiter_middleware.dart @@ -16,7 +16,7 @@ String? _getIpAddress(RequestContext context) { return xff; } // Fallback to the direct connection IP if XFF is not available. - return context.request.connectionInfo?.remoteAddress.address; + return context.request.connectionInfo.remoteAddress.address; } /// Middleware to enforce rate limiting on a route. @@ -59,7 +59,7 @@ Middleware rateLimiter({ }; } -/// A specific implementation of the [keyExtractor] for IP-based rate limiting. +/// A specific implementation of the keyExtractor for IP-based rate limiting. Future ipKeyExtractor(RequestContext context) async { return _getIpAddress(context); } diff --git a/lib/src/services/mongodb_rate_limit_service.dart b/lib/src/services/mongodb_rate_limit_service.dart index 3817166..fb6f1ff 100644 --- a/lib/src/services/mongodb_rate_limit_service.dart +++ b/lib/src/services/mongodb_rate_limit_service.dart @@ -55,7 +55,7 @@ class MongoDbRateLimitService implements RateLimitService { 'Rate limit exceeded for key "$key". ' '($recentRequestsCount >= $limit)', ); - throw ForbiddenException( + throw const ForbiddenException( 'You have made too many requests. Please try again later.', ); } @@ -72,7 +72,7 @@ class MongoDbRateLimitService implements RateLimitService { rethrow; } catch (e, s) { _log.severe('Error during rate limit check for key "$key"', e, s); - throw OperationFailedException( + throw const OperationFailedException( 'An unexpected error occurred while checking request rate limits.', ); } From f09468fa4edcaebb353eeb25b3b4c78ad1bf9ede Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 18:25:14 +0100 Subject: [PATCH 10/21] chore(env): add rate limiting configuration variables - Add new environment variables for rate limiting configuration - Include settings for /auth/request-code and /data API endpoints - Specify limit and time window for each endpoint --- .env.example | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.env.example b/.env.example index 6cbadc3..d293784 100644 --- a/.env.example +++ b/.env.example @@ -43,3 +43,12 @@ # - If an admin with this email already exists, nothing changes. # This provides a secure way to set or recover the admin account. # OVERRIDE_ADMIN_EMAIL="admin@example.com" + +# --- Rate Limiting --- +# Configuration for the /auth/request-code endpoint. +RATE_LIMIT_REQUEST_CODE_LIMIT=3 +RATE_LIMIT_REQUEST_CODE_WINDOW_HOURS=24 + +# Configuration for the /data API endpoints. +RATE_LIMIT_DATA_API_LIMIT=1000 +RATE_LIMIT_DATA_API_WINDOW_MINUTES=60 From 9453122b659263cff78b215907140553adaa8dc4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 18:25:51 +0100 Subject: [PATCH 11/21] feat(config): add rate limit configuration parameters - Introduce new environment variables for request-code and data API rate limiting - Implement getters for rate limit parameters with default values - Add documentation for new configuration options --- lib/src/config/environment_config.dart | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lib/src/config/environment_config.dart b/lib/src/config/environment_config.dart index d65fc70..30df0e1 100644 --- a/lib/src/config/environment_config.dart +++ b/lib/src/config/environment_config.dart @@ -139,4 +139,37 @@ abstract final class EnvironmentConfig { /// This is used to set or replace the single administrator account on startup. /// Returns `null` if the `OVERRIDE_ADMIN_EMAIL` is not set. static String? get overrideAdminEmail => _env['OVERRIDE_ADMIN_EMAIL']; + + /// Retrieves the request limit for the request-code endpoint. + /// + /// Defaults to 3 if not set or if parsing fails. + static int get rateLimitRequestCodeLimit { + return int.tryParse(_env['RATE_LIMIT_REQUEST_CODE_LIMIT'] ?? '3') ?? 3; + } + + /// Retrieves the time window for the request-code endpoint rate limit. + /// + /// Defaults to 24 hours if not set or if parsing fails. + static Duration get rateLimitRequestCodeWindow { + final hours = + int.tryParse(_env['RATE_LIMIT_REQUEST_CODE_WINDOW_HOURS'] ?? '24') ?? + 24; + return Duration(hours: hours); + } + + /// Retrieves the request limit for the data API endpoints. + /// + /// Defaults to 1000 if not set or if parsing fails. + static int get rateLimitDataApiLimit { + return int.tryParse(_env['RATE_LIMIT_DATA_API_LIMIT'] ?? '1000') ?? 1000; + } + + /// Retrieves the time window for the data API rate limit. + /// + /// Defaults to 60 minutes if not set or if parsing fails. + static Duration get rateLimitDataApiWindow { + final minutes = + int.tryParse(_env['RATE_LIMIT_DATA_API_WINDOW_MINUTES'] ?? '60') ?? 60; + return Duration(minutes: minutes); + } } From 65f76a22a62298404d8e29df6f07c25254304e3b Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 18:30:02 +0100 Subject: [PATCH 12/21] refactor(auth): move request-code handler to index.dart and apply rate limiting - Rename request-code.dart to index.dart for better modularity - Implement rate limiting middleware directly in the handler - Improve code structure and prepare for additional endpoint implementations --- .../index.dart} | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) rename routes/api/v1/auth/{request-code.dart => request-code/index.dart} (88%) diff --git a/routes/api/v1/auth/request-code.dart b/routes/api/v1/auth/request-code/index.dart similarity index 88% rename from routes/api/v1/auth/request-code.dart rename to routes/api/v1/auth/request-code/index.dart index 57b16bd..a5d653a 100644 --- a/routes/api/v1/auth/request-code.dart +++ b/routes/api/v1/auth/request-code/index.dart @@ -2,14 +2,28 @@ import 'dart:io'; import 'package:core/core.dart'; // For exceptions import 'package:dart_frog/dart_frog.dart'; -import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/rate_limiter_middleware.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/services/auth_service.dart'; import 'package:logging/logging.dart'; // Create a logger for this file. final _logger = Logger('request_code_handler'); -Future _onRequest(RequestContext context) async { +/// Handles POST requests to `/api/v1/auth/request-code`. +/// +/// Initiates an email-based sign-in process. This endpoint is context-aware. +/// +/// - For the user-facing app, it sends a verification code to the provided +/// email, supporting both sign-in and sign-up. +/// - For the dashboard, the request body must include `"isDashboardLogin": true`. +/// In this mode, it first verifies the user exists and has 'admin' or +/// 'publisher' roles before sending a code, effectively acting as a +/// login-only gate. +Future onRequest(RequestContext context) async { + // Ensure this is a POST request + if (context.request.method != HttpMethod.post) { + return Response(statusCode: HttpStatus.methodNotAllowed); + } + // Read the AuthService provided by middleware final authService = context.read(); @@ -79,30 +93,4 @@ Future _onRequest(RequestContext context) async { } } -/// Handles POST requests to `/api/v1/auth/request-code`. -/// -/// Initiates an email-based sign-in process. This endpoint is context-aware. -/// -/// - For the user-facing app, it sends a verification code to the provided -/// email, supporting both sign-in and sign-up. -/// - For the dashboard, the request body must include `"isDashboardLogin": true`. -/// In this mode, it first verifies the user exists and has 'admin' or -/// 'publisher' roles before sending a code, effectively acting as a -/// login-only gate. -Future onRequest(RequestContext context) async { - // Ensure this is a POST request - if (context.request.method != HttpMethod.post) { - return Response(statusCode: HttpStatus.methodNotAllowed); - } - // Apply the rate limiter middleware before calling the actual handler. - final handler = const Pipeline().addMiddleware( - rateLimiter( - limit: 3, - window: const Duration(hours: 24), - keyExtractor: ipKeyExtractor, - ), - ).addHandler(_onRequest); - - return handler(context); -} From 9fd3a44c81fe6a2774d300618f3dbde499936a13 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 18:30:15 +0100 Subject: [PATCH 13/21] feat(auth): add rate limiting middleware to request code endpoint - Implement rate limiting specifically for the `/api/v1/auth/request-code` endpoint - Use custom rate limit configuration from EnvironmentConfig - Apply rateLimiter middleware with ipKeyExtractor for key generation --- routes/api/v1/auth/request-code/_middleware.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 routes/api/v1/auth/request-code/_middleware.dart diff --git a/routes/api/v1/auth/request-code/_middleware.dart b/routes/api/v1/auth/request-code/_middleware.dart new file mode 100644 index 0000000..160d33f --- /dev/null +++ b/routes/api/v1/auth/request-code/_middleware.dart @@ -0,0 +1,15 @@ +import 'package:dart_frog/dart_frog.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/config/environment_config.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/rate_limiter_middleware.dart'; + +/// This middleware applies a rate limit specifically to the +/// `/api/v1/auth/request-code` endpoint. +Handler middleware(Handler handler) { + return handler.use( + rateLimiter( + limit: EnvironmentConfig.rateLimitRequestCodeLimit, + window: EnvironmentConfig.rateLimitRequestCodeWindow, + keyExtractor: ipKeyExtractor, + ), + ); +} From 17df05c30872e0aef2ae69f0e0b3990eea383be1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 18:30:29 +0100 Subject: [PATCH 14/21] style(auth): remove extra whitespace in request-code handler - Reduced the whitespace between blocks in the `onRequest` function - Improved code readability and formatting without changing functionality --- routes/api/v1/auth/request-code/index.dart | 42 +++++++++++----------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/routes/api/v1/auth/request-code/index.dart b/routes/api/v1/auth/request-code/index.dart index a5d653a..c7b45cc 100644 --- a/routes/api/v1/auth/request-code/index.dart +++ b/routes/api/v1/auth/request-code/index.dart @@ -70,27 +70,25 @@ Future onRequest(RequestContext context) async { } try { - // Call the AuthService to handle the logic, passing the context flag. - await authService.initiateEmailSignIn( - email, - isDashboardLogin: isDashboardLogin, - ); + // Call the AuthService to handle the logic, passing the context flag. + await authService.initiateEmailSignIn( + email, + isDashboardLogin: isDashboardLogin, + ); - // Return 202 Accepted: The request is accepted for processing, - // but the processing (email sending) hasn't necessarily completed. - // 200 OK is also acceptable if you consider the API call itself complete. - return Response(statusCode: HttpStatus.accepted); -} on HttpException catch (_) { - // Let the central errorHandler middleware handle known exceptions - rethrow; -} catch (e, s) { - // Catch unexpected errors from the service layer - _logger.severe('Unexpected error in /request-code handler', e, s); - // Let the central errorHandler handle this as a 500 - throw const OperationFailedException( - 'An unexpected error occurred while requesting the sign-in code.', - ); -} + // Return 202 Accepted: The request is accepted for processing, + // but the processing (email sending) hasn't necessarily completed. + // 200 OK is also acceptable if you consider the API call itself complete. + return Response(statusCode: HttpStatus.accepted); + } on HttpException catch (_) { + // Let the central errorHandler middleware handle known exceptions + rethrow; + } catch (e, s) { + // Catch unexpected errors from the service layer + _logger.severe('Unexpected error in /request-code handler', e, s); + // Let the central errorHandler handle this as a 500 + throw const OperationFailedException( + 'An unexpected error occurred while requesting the sign-in code.', + ); + } } - - From 6134a81531924a8caf31e4085f1c8f391ba7661e Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 18:30:50 +0100 Subject: [PATCH 15/21] feat(rbac): add rate limiting bypass permission - Add new permission 'rate_limiting.bypass' to permissions.dart - Grant this new permission to dashboard publisher and admin roles in role_permissions.dart --- lib/src/rbac/permissions.dart | 3 +++ lib/src/rbac/role_permissions.dart | 2 ++ 2 files changed, 5 insertions(+) diff --git a/lib/src/rbac/permissions.dart b/lib/src/rbac/permissions.dart index 54ece71..c278f54 100644 --- a/lib/src/rbac/permissions.dart +++ b/lib/src/rbac/permissions.dart @@ -61,4 +61,7 @@ abstract class Permissions { // User Preference Permissions static const String userPreferenceBypassLimits = 'user_preference.bypass_limits'; + + // General System Permissions + static const String rateLimitingBypass = 'rate_limiting.bypass'; } diff --git a/lib/src/rbac/role_permissions.dart b/lib/src/rbac/role_permissions.dart index e3cc0b9..99b275f 100644 --- a/lib/src/rbac/role_permissions.dart +++ b/lib/src/rbac/role_permissions.dart @@ -34,10 +34,12 @@ final Set _dashboardPublisherPermissions = { Permissions.headlineUpdate, Permissions.headlineDelete, Permissions.dashboardLogin, + Permissions.rateLimitingBypass, }; final Set _dashboardAdminPermissions = { ..._dashboardPublisherPermissions, + Permissions.rateLimitingBypass, Permissions.topicCreate, Permissions.topicUpdate, Permissions.topicDelete, From baec763f3781d83a0c048f3e0d8314fd8f89415e Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 18:35:36 +0100 Subject: [PATCH 16/21] feat(api): implement rate limiting for data routes - Add rate limiting middleware for /api/v1/data routes - Implement bypass permission for rate limiting - Configure rate limit using environment variables - Update middleware documentation and comments --- routes/api/v1/data/_middleware.dart | 64 +++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/routes/api/v1/data/_middleware.dart b/routes/api/v1/data/_middleware.dart index 2eaa41d..142ec0a 100644 --- a/routes/api/v1/data/_middleware.dart +++ b/routes/api/v1/data/_middleware.dart @@ -1,7 +1,11 @@ import 'package:core/core.dart'; import 'package:dart_frog/dart_frog.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/config/environment_config.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/authentication_middleware.dart'; -import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/authorization_middleware.dart'; // Import authorization middleware +import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/authorization_middleware.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/middlewares/rate_limiter_middleware.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/rbac/permission_service.dart'; +import 'package:flutter_news_app_api_server_full_source_code/src/rbac/permissions.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/registry/model_registry.dart'; /// Middleware specific to the generic `/api/v1/data` route path. @@ -9,12 +13,13 @@ import 'package:flutter_news_app_api_server_full_source_code/src/registry/model_ /// This middleware chain performs the following in order: /// 1. **Authentication Check (`requireAuthentication`):** Ensures that the user /// is authenticated. If not, it aborts the request with a 401. -/// 2. **Model Validation & Context Provision (`_modelValidationAndProviderMiddleware`):** +/// 2. **Data Rate Limiting (`_dataRateLimiterMiddleware`):** Applies a +/// configurable, user-centric rate limit. Bypassed by admin/publisher roles. +/// 3. **Model Validation & Context Provision (`_modelValidationAndProviderMiddleware`):** /// - Validates the `model` query parameter. /// - Looks up the `ModelConfig` from the `ModelRegistryMap`. -/// - Provides the `ModelConfig` and `modelName` into the request context -/// for downstream middleware and route handlers. -/// 3. **Authorization Check (`authorizationMiddleware`):** Enforces role-based +/// - Provides the `ModelConfig` and `modelName` into the request context. +/// 4. **Authorization Check (`authorizationMiddleware`):** Enforces role-based /// and model-specific permissions based on the `ModelConfig` metadata. /// If the user lacks permission, it throws a [ForbiddenException]. /// @@ -22,6 +27,34 @@ import 'package:flutter_news_app_api_server_full_source_code/src/registry/model_ /// model-specific configuration available, and access is authorized before /// reaching the final route handler. +// Helper middleware for applying rate limiting to the data routes. +Middleware _dataRateLimiterMiddleware() { + return (handler) { + return (context) { + final user = context.read(); + final permissionService = context.read(); + + // Users with the bypass permission are not rate-limited. + if (permissionService.hasPermission( + user, + Permissions.rateLimitingBypass, + )) { + return handler(context); + } + + // For all other users, apply the configured rate limit. + // The key is the user's ID, ensuring the limit is per-user. + final rateLimitHandler = rateLimiter( + limit: EnvironmentConfig.rateLimitDataApiLimit, + window: EnvironmentConfig.rateLimitDataApiWindow, + keyExtractor: (context) async => context.read().id, + )(handler); + + return rateLimitHandler(context); + }; + }; +} + // Helper middleware for model validation and context provision. Middleware _modelValidationAndProviderMiddleware() { return (handler) { @@ -79,15 +112,19 @@ Handler middleware(Handler handler) { // resulting in a 401 response via the global `errorHandler`). // - If `User` is present, the request proceeds to the next middleware. // - // 2. `_modelValidationAndProviderMiddleware()`: + // 2. `_dataRateLimiterMiddleware()`: // - This runs if `requireAuthentication()` passes. + // - It checks if the user has a bypass permission. If not, it applies + // the configured rate limit based on the user's ID. + // - If the limit is exceeded, it throws a `ForbiddenException`. + // + // 3. `_modelValidationAndProviderMiddleware()`: + // - This runs if rate limiting passes. // - It validates the `?model=` query parameter and provides the // `ModelConfig` and `modelName` into the context. - // - If model validation fails, it throws a BadRequestException, caught - // by the global errorHandler. - // - If successful, it calls the next handler in the chain (authorizationMiddleware). + // - If model validation fails, it throws a `BadRequestException`. // - // 3. `authorizationMiddleware()`: + // 4. `authorizationMiddleware()`: // - This runs if `_modelValidationAndProviderMiddleware()` passes. // - It reads the `User`, `modelName`, and `ModelConfig` from the context. // - It checks if the user has permission to perform the requested HTTP @@ -97,14 +134,15 @@ Handler middleware(Handler handler) { // - If successful, it calls the next handler in the chain (the actual // route handler). // - // 4. Actual Route Handler (from `index.dart` or `[id].dart`): + // 5. Actual Route Handler (from `index.dart` or `[id].dart`): // - This runs last, only if all preceding middlewares pass. It will have // access to a non-null `User`, `ModelConfig`, and `modelName` from the context. // - It performs the data operation and any necessary handler-level // ownership checks (if flagged by `ModelActionPermission.requiresOwnershipCheck`). // return handler - .use(authorizationMiddleware()) // Applied third (inner) - .use(_modelValidationAndProviderMiddleware()) // Applied second + .use(authorizationMiddleware()) // Applied fourth (inner-most) + .use(_modelValidationAndProviderMiddleware()) // Applied third + .use(_dataRateLimiterMiddleware()) // Applied second .use(requireAuthentication()); // Applied first (outermost) } From 8f7a85a045e19e0cdb022d3a21e5ba6c5ba7491f Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 18:37:53 +0100 Subject: [PATCH 17/21] refactor(routes): remove redundant middleware documentation - Removed detailed documentation from _middleware.dart file - Kept existing middleware functions intact --- routes/api/v1/data/_middleware.dart | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/routes/api/v1/data/_middleware.dart b/routes/api/v1/data/_middleware.dart index 142ec0a..c5b67f9 100644 --- a/routes/api/v1/data/_middleware.dart +++ b/routes/api/v1/data/_middleware.dart @@ -8,25 +8,6 @@ import 'package:flutter_news_app_api_server_full_source_code/src/rbac/permission import 'package:flutter_news_app_api_server_full_source_code/src/rbac/permissions.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/registry/model_registry.dart'; -/// Middleware specific to the generic `/api/v1/data` route path. -/// -/// This middleware chain performs the following in order: -/// 1. **Authentication Check (`requireAuthentication`):** Ensures that the user -/// is authenticated. If not, it aborts the request with a 401. -/// 2. **Data Rate Limiting (`_dataRateLimiterMiddleware`):** Applies a -/// configurable, user-centric rate limit. Bypassed by admin/publisher roles. -/// 3. **Model Validation & Context Provision (`_modelValidationAndProviderMiddleware`):** -/// - Validates the `model` query parameter. -/// - Looks up the `ModelConfig` from the `ModelRegistryMap`. -/// - Provides the `ModelConfig` and `modelName` into the request context. -/// 4. **Authorization Check (`authorizationMiddleware`):** Enforces role-based -/// and model-specific permissions based on the `ModelConfig` metadata. -/// If the user lacks permission, it throws a [ForbiddenException]. -/// -/// This setup ensures that data routes are protected, have the necessary -/// model-specific configuration available, and access is authorized before -/// reaching the final route handler. - // Helper middleware for applying rate limiting to the data routes. Middleware _dataRateLimiterMiddleware() { return (handler) { From cd5e29bc739e95062be2bc95e7df3639a1e0a2bc Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 18:41:12 +0100 Subject: [PATCH 18/21] docs(env): clarify rate limiting configuration in .env.example - Add explanation for optional rate limiting configuration - Provide default values and units for rate limit settings - Improve clarity on purpose and usage of rate limiting options --- .env.example | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index d293784..e8ba7cc 100644 --- a/.env.example +++ b/.env.example @@ -44,11 +44,15 @@ # This provides a secure way to set or recover the admin account. # OVERRIDE_ADMIN_EMAIL="admin@example.com" -# --- Rate Limiting --- -# Configuration for the /auth/request-code endpoint. -RATE_LIMIT_REQUEST_CODE_LIMIT=3 -RATE_LIMIT_REQUEST_CODE_WINDOW_HOURS=24 - -# Configuration for the /data API endpoints. -RATE_LIMIT_DATA_API_LIMIT=1000 -RATE_LIMIT_DATA_API_WINDOW_MINUTES=60 +# OPTIONAL: Configure API request limits to prevent abuse. +# The application provides sensible defaults if these are not set. +# +# Limit for the /auth/request-code endpoint (requests per window). +# RATE_LIMIT_REQUEST_CODE_LIMIT=3 +# Window for the /auth/request-code endpoint, in hours. +# RATE_LIMIT_REQUEST_CODE_WINDOW_HOURS=24 +# +# Limit for the generic /data API endpoints (requests per window). +# RATE_LIMIT_DATA_API_LIMIT=1000 +# Window for the /data API endpoints, in minutes. +# RATE_LIMIT_DATA_API_WINDOW_MINUTES=60 From 0377190608acb92edefa817646def1cfa7d47355 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 18:41:22 +0100 Subject: [PATCH 19/21] docs(README): add built-in API rate limiting features - Add information about built-in API rate limiting to README.md - Highlight protection against abuse and denial-of-service attacks - Mention configurable, user-aware limits and trusted role bypass - Emphasize stability and cost prevention benefits --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index cecf467..c3f8b08 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,12 @@ This API server comes packed with all the features you need to launch a professi * Securely control access to API features and data management capabilities based on user roles. > **Your Advantage:** A powerful, built-in security model that protects your data and ensures users only access what they're supposed to. 🛡️ +#### 🛡️ **Built-in API Rate Limiting** +* Protects critical endpoints like email verification and data access from abuse and denial-of-service attacks. +* Features configurable, user-aware limits that distinguish between guests and authenticated users. +* Includes a bypass for trusted roles (admin, publisher) to ensure dashboard functionality is never impeded. +> **Your Advantage:** Your API is protected from day one against common abuse vectors, ensuring stability and preventing costly overages on services like email providers. ✅ + #### ⚙️ **Centralized App & User Settings** * Effortlessly sync user-specific settings like theme, language, and font styles across devices. * Manage personalized content preferences, including saved headlines and followed topics/sources. From 17fc972ac1582d9d45eec4960f08553b9d00080a Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 18:53:11 +0100 Subject: [PATCH 20/21] fix(config): update default JWT expiry hours to 1 month - Change default JWT expiry hours from 1 hour to 720 hours (1 month) - Modify the fallback value in int.tryParse from '1' to '720' - This change affects the jwtExpiryDuration getter in EnvironmentConfig class --- lib/src/config/environment_config.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/config/environment_config.dart b/lib/src/config/environment_config.dart index 30df0e1..81147df 100644 --- a/lib/src/config/environment_config.dart +++ b/lib/src/config/environment_config.dart @@ -109,7 +109,7 @@ abstract final class EnvironmentConfig { /// The value is read from the `JWT_EXPIRY_HOURS` environment variable. /// Defaults to 1 hour if not set or if parsing fails. static Duration get jwtExpiryDuration { - final hours = int.tryParse(_env['JWT_EXPIRY_HOURS'] ?? '1'); + final hours = int.tryParse(_env['JWT_EXPIRY_HOURS'] ?? '720'); // 1 month return Duration(hours: hours ?? 1); } From 0bab60de0a3e9c26b8e3ff074948d000744277b4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 29 Jul 2025 18:53:31 +0100 Subject: [PATCH 21/21] docs(env): update JWT expiry default and admin configuration - Change JWT_EXPIRY_HOURS default from 1 hour to 720 hours (1 month) - Update ADMIN OVERRIDE to REQUIRED for the single administrator account setting - Clarify optional rate limit configuration for API endpoints --- .env.example | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index e8ba7cc..9bcd43e 100644 --- a/.env.example +++ b/.env.example @@ -12,8 +12,8 @@ # JWT_SECRET_KEY="your-super-secret-and-long-jwt-key" # OPTIONAL: The duration for which a JWT is valid, in hours. -# Defaults to 1 hour if not specified. -# JWT_EXPIRY_HOURS="1" +# Defaults to 720 hour (1 month) if not specified. +# JWT_EXPIRY_HOURS="720" # REQUIRED FOR PRODUCTION: The specific origin URL of your web client. # This allows the client (e.g., the HT Dashboard) to make requests to the API. @@ -34,7 +34,7 @@ # Use "https://api.eu.sendgrid.com" for EU-based accounts. # SENDGRID_API_URL="https://api.sendgrid.com" -# ADMIN OVERRIDE: Sets the single administrator account for the application. +# REQUIRED: Sets the single administrator account for the application. # On server startup, the system ensures that the user with this email is the # one and only administrator. # - If no admin exists, one will be created with this email. @@ -44,15 +44,15 @@ # This provides a secure way to set or recover the admin account. # OVERRIDE_ADMIN_EMAIL="admin@example.com" -# OPTIONAL: Configure API request limits to prevent abuse. -# The application provides sensible defaults if these are not set. -# -# Limit for the /auth/request-code endpoint (requests per window). + +# OPTIONAL: Limit for the /auth/request-code endpoint (requests per window). # RATE_LIMIT_REQUEST_CODE_LIMIT=3 -# Window for the /auth/request-code endpoint, in hours. + +# OPTIONAL: Window for the /auth/request-code endpoint, in hours. # RATE_LIMIT_REQUEST_CODE_WINDOW_HOURS=24 -# -# Limit for the generic /data API endpoints (requests per window). + +# OPTIONAL: Limit for the generic /data API endpoints (requests per window). # RATE_LIMIT_DATA_API_LIMIT=1000 -# Window for the /data API endpoints, in minutes. + +# OPTIONAL: Window for the /data API endpoints, in minutes. # RATE_LIMIT_DATA_API_WINDOW_MINUTES=60