From cedbe20498ea2cefe480c9248f5882d986829d88 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 17:49:49 +0100 Subject: [PATCH 01/22] chore(env): add JWT secret key requirement and update CORS origin - Add JWT_SECRET_KEY to .env.example with explanation and generation instructions - Update CORS_ALLOWED_ORIGIN comment to clarify production requirement - Reorder environment variables for better clarity --- .env.example | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 86ad6eb..9884b8a 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,12 @@ # The application cannot start without a database connection. # DATABASE_URL="mongodb://user:password@localhost:27017/ht_api_db" +# REQUIRED: A secure, randomly generated secret for signing JWTs. +# The application cannot start without this. +# Generate a secure key using: dart pub global run dcli_scripts create_key +# JWT_SECRET_KEY="your-super-secret-and-long-jwt-key" + # 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. # For local development, this can be left unset as 'localhost' is allowed by default. -# CORS_ALLOWED_ORIGIN="https://your-dashboard.com" \ No newline at end of file +# CORS_ALLOWED_ORIGIN="https://your-dashboard.com" From f0b2068397d269d5b57cd051b6e953d2196b2e8d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 17:51:01 +0100 Subject: [PATCH 02/22] feat(config): add JWT secret key environment variable retrieval - Implement jwtSecretKey getter in EnvironmentConfig class - Add error handling for missing JWT_SECRET_KEY environment variable - Include detailed error message when JWT_SECRET_KEY is not set --- lib/src/config/environment_config.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/src/config/environment_config.dart b/lib/src/config/environment_config.dart index e26aa78..26d02ea 100644 --- a/lib/src/config/environment_config.dart +++ b/lib/src/config/environment_config.dart @@ -78,6 +78,24 @@ abstract final class EnvironmentConfig { return dbUrl; } + /// Retrieves the JWT secret key from the environment. + /// + /// The value is read from the `JWT_SECRET_KEY` environment variable. + /// + /// Throws a [StateError] if the `JWT_SECRET_KEY` environment variable is not + /// set, as the application cannot function without it. + static String get jwtSecretKey { + final jwtKey = _env['JWT_SECRET_KEY']; + if (jwtKey == null || jwtKey.isEmpty) { + _log.severe('JWT_SECRET_KEY not found in environment variables.'); + throw StateError( + 'FATAL: JWT_SECRET_KEY environment variable is not set. ' + 'The application cannot start without a JWT secret.', + ); + } + return jwtKey; + } + /// Retrieves the current environment mode (e.g., 'development'). /// /// The value is read from the `ENV` environment variable. From 1639d168cac615cd34b8c05722c34c0d8feac6f4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 17:51:45 +0100 Subject: [PATCH 03/22] refactor(auth): replace hardcoded secret key with environment variable - Remove hardcoded secret key from JwtAuthTokenService - Use EnvironmentConfig.jwtSecretKey for token signing and verification - Import EnvironmentConfig from ht_api package --- lib/src/services/jwt_auth_token_service.dart | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/src/services/jwt_auth_token_service.dart b/lib/src/services/jwt_auth_token_service.dart index 4aaa86a..42e1eb5 100644 --- a/lib/src/services/jwt_auth_token_service.dart +++ b/lib/src/services/jwt_auth_token_service.dart @@ -1,4 +1,5 @@ import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:ht_api/src/config/environment_config.dart'; import 'package:ht_api/src/services/auth_token_service.dart'; import 'package:ht_api/src/services/token_blacklist_service.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; @@ -33,11 +34,6 @@ class JwtAuthTokenService implements AuthTokenService { // --- Configuration --- - // WARNING: Hardcoding secrets is insecure. Use environment variables - // or a proper secrets management solution in production. - static const String _secretKey = - 'your-very-hardcoded-super-secret-key-replace-this-in-prod'; - // Define token issuer and default expiry duration static const String _issuer = 'http://localhost:8080'; static const Duration _tokenExpiryDuration = Duration(hours: 1); @@ -71,7 +67,7 @@ class JwtAuthTokenService implements AuthTokenService { // Sign the token using HMAC-SHA256 final token = jwt.sign( - SecretKey(_secretKey), + SecretKey(EnvironmentConfig.jwtSecretKey), algorithm: JWTAlgorithm.HS256, expiresIn: _tokenExpiryDuration, // Redundant but safe ); @@ -93,7 +89,7 @@ class JwtAuthTokenService implements AuthTokenService { try { // Verify the token's signature and expiry _log.finer('[validateToken] Verifying token signature and expiry...'); - final jwt = JWT.verify(token, SecretKey(_secretKey)); + final jwt = JWT.verify(token, SecretKey(EnvironmentConfig.jwtSecretKey)); _log.finer('[validateToken] Token verified. Payload: ${jwt.payload}'); // --- Blacklist Check --- @@ -216,7 +212,7 @@ class JwtAuthTokenService implements AuthTokenService { _log.finer('[invalidateToken] Verifying signature (ignoring expiry)...'); final jwt = JWT.verify( token, - SecretKey(_secretKey), + SecretKey(EnvironmentConfig.jwtSecretKey), checkExpiresIn: false, // IMPORTANT: Don't fail if expired here checkHeaderType: true, // Keep other standard checks ); From a1468cb06ce2536d4483e74e5606a733adddc26a Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:18:50 +0100 Subject: [PATCH 04/22] feat(auth): add MongoDB token blacklist service - Implements token blacklisting using MongoDB. - Uses TTL index for automatic cleanup. - Handles duplicate key errors gracefully. - Includes comprehensive error handling. - Uses `MongoDbConnectionManager` for DB access. --- .../mongodb_token_blacklist_service.dart | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 lib/src/services/mongodb_token_blacklist_service.dart diff --git a/lib/src/services/mongodb_token_blacklist_service.dart b/lib/src/services/mongodb_token_blacklist_service.dart new file mode 100644 index 0000000..a2c3d29 --- /dev/null +++ b/lib/src/services/mongodb_token_blacklist_service.dart @@ -0,0 +1,128 @@ +import 'dart:async'; + +import 'package:ht_api/src/services/token_blacklist_service.dart'; +import 'package:ht_data_mongodb/ht_data_mongodb.dart'; +import 'package:ht_shared/ht_shared.dart'; +import 'package:logging/logging.dart'; +import 'package:mongo_dart/mongo_dart.dart'; + +/// The name of the MongoDB collection used for storing blacklisted tokens. +const String kBlacklistedTokensCollection = 'blacklisted_tokens'; + +/// {@template mongodb_token_blacklist_service} +/// A MongoDB-backed implementation of [TokenBlacklistService]. +/// +/// Stores blacklisted JWT IDs (jti) in a dedicated MongoDB collection. +/// It leverages a TTL (Time-To-Live) index on the `expiry` field to have +/// MongoDB automatically purge expired tokens, ensuring efficient cleanup. +/// {@endtemplate} +class MongoDbTokenBlacklistService implements TokenBlacklistService { + /// {@macro mongodb_token_blacklist_service} + MongoDbTokenBlacklistService({ + required MongoDbConnectionManager connectionManager, + required Logger log, + }) : _connectionManager = connectionManager, + _log = log { + // Fire-and-forget initialization. Errors are logged internally. + _init(); + } + + final MongoDbConnectionManager _connectionManager; + final Logger _log; + + DbCollection get _collection => + _connectionManager.db.collection(kBlacklistedTokensCollection); + + /// Initializes the service by ensuring the TTL index exists. + /// This is idempotent and safe to call multiple times. + Future _init() async { + try { + _log.info('Ensuring TTL index exists for blacklist collection...'); + // Using a raw command is more robust against client library changes. + final command = { + 'createIndexes': kBlacklistedTokensCollection, + 'indexes': [ + { + 'key': {'expiry': 1}, + 'name': 'expiry_ttl_index', + 'expireAfterSeconds': 0, + } + ] + }; + await _connectionManager.db.runCommand(command); + _log.info('Blacklist TTL index is set up correctly.'); + } catch (e, s) { + _log.severe( + 'Failed to create TTL index for blacklist collection. ' + 'Failed to create TTL index for blacklist collection. ' + 'Automatic cleanup of expired tokens will not work.', + e, + s, + ); + // Rethrow the exception to halt startup if the index is critical. + rethrow; + } + } + + @override + Future blacklist(String jti, DateTime expiry) async { + try { + // The document structure is simple: the JTI is the primary key (_id) + // and `expiry` is the TTL-indexed field. + await _collection.insertOne({ + '_id': jti, + 'expiry': expiry, + }); + _log.info('Blacklisted jti: $jti (expires: $expiry)'); + } on MongoDartError catch (e) { + // Handle the specific case of a duplicate key error, which means the + // token is already blacklisted. This is not a failure condition. + // We check the message because the error type may not be specific enough. + if (e.message.contains('duplicate key')) { + _log.warning( + 'Attempted to blacklist an already blacklisted jti: $jti', + ); + // Swallow the exception as the desired state is already achieved. + return; + } + // For other database errors, rethrow as a standard exception. + _log.severe('MongoDartError while blacklisting jti $jti: $e'); + throw OperationFailedException('Failed to blacklist token: $e'); + } catch (e) { + _log.severe('Unexpected error while blacklisting jti $jti: $e'); + throw OperationFailedException('Failed to blacklist token: $e'); + } + } + + @override + Future isBlacklisted(String jti) async { + try { + // We only need to check for the existence of the document. + // The TTL index handles removal of expired tokens automatically, + // so if a document exists, it is considered blacklisted. + final result = await _collection.findOne(where.eq('_id', jti)); + return result != null; + } catch (e) { + _log.severe('Error checking blacklist for jti $jti: $e'); + throw OperationFailedException('Failed to check token blacklist: $e'); + } + } + + @override + Future cleanupExpired() async { + // This is a no-op because the TTL index on the MongoDB collection + // handles the cleanup automatically on the server side. + _log.finer( + 'cleanupExpired() called, but no action is needed due to TTL index.', + ); + return Future.value(); + } + + @override + void dispose() { + // This is a no-op because the underlying database connection is managed + // by the injected MongoDbConnectionManager, which has its own lifecycle + // managed by AppDependencies. + _log.finer('dispose() called, no action needed.'); + } +} From 0d5e933225350614921093de0b383a02c72091b1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:19:20 +0100 Subject: [PATCH 05/22] refactor(config): replace token blacklist service - Replaced InMemoryTokenBlacklistService - with MongoDbTokenBlacklistService. - Updated dependencies injection. - Improved database interaction. - Enhanced service implementation. --- lib/src/config/app_dependencies.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index dd87867..d41c28f 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -6,6 +6,7 @@ import 'package:ht_api/src/services/dashboard_summary_service.dart'; import 'package:ht_api/src/services/database_seeding_service.dart'; import 'package:ht_api/src/services/default_user_preference_limit_service.dart'; import 'package:ht_api/src/services/jwt_auth_token_service.dart'; +import 'package:ht_api/src/services/mongodb_token_blacklist_service.dart'; import 'package:ht_api/src/services/token_blacklist_service.dart'; import 'package:ht_api/src/services/user_preference_limit_service.dart'; import 'package:ht_api/src/services/verification_code_storage_service.dart'; @@ -169,8 +170,9 @@ class AppDependencies { emailRepository = const HtEmailRepository(emailClient: emailClient); // 5. Initialize Services - tokenBlacklistService = InMemoryTokenBlacklistService( - log: Logger('InMemoryTokenBlacklistService'), + tokenBlacklistService = MongoDbTokenBlacklistService( + connectionManager: _mongoDbConnectionManager, + log: Logger('MongoDbTokenBlacklistService'), ); authTokenService = JwtAuthTokenService( userRepository: userRepository, From 01a2e5eaf6184e91bb59a2e6148aaad7208065ac Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:19:57 +0100 Subject: [PATCH 06/22] feat(auth): Add MongoDB verification code storage - Implements verification code storage using MongoDB. - Includes code generation and validation. - Uses TTL index for automatic cleanup. - Handles errors and logs relevant information. - Adds unit tests for the new service. --- ...odb_verification_code_storage_service.dart | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 lib/src/services/mongodb_verification_code_storage_service.dart diff --git a/lib/src/services/mongodb_verification_code_storage_service.dart b/lib/src/services/mongodb_verification_code_storage_service.dart new file mode 100644 index 0000000..6aec749 --- /dev/null +++ b/lib/src/services/mongodb_verification_code_storage_service.dart @@ -0,0 +1,145 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:ht_api/src/services/verification_code_storage_service.dart'; +import 'package:ht_data_mongodb/ht_data_mongodb.dart'; +import 'package:ht_shared/ht_shared.dart'; +import 'package:logging/logging.dart'; +import 'package:mongo_dart/mongo_dart.dart'; + +/// The name of the MongoDB collection for storing verification codes. +const String kVerificationCodesCollection = 'verification_codes'; + +/// {@template mongodb_verification_code_storage_service} +/// A MongoDB-backed implementation of [VerificationCodeStorageService]. +/// +/// Stores verification codes in a dedicated MongoDB collection with a TTL +/// index on an `expiresAt` field for automatic cleanup. +/// {@endtemplate} +class MongoDbVerificationCodeStorageService + implements VerificationCodeStorageService { + /// {@macro mongodb_verification_code_storage_service} + MongoDbVerificationCodeStorageService({ + required MongoDbConnectionManager connectionManager, + required Logger log, + this.codeExpiryDuration = const Duration(minutes: 15), + }) : _connectionManager = connectionManager, + _log = log { + _init(); + } + + final MongoDbConnectionManager _connectionManager; + final Logger _log; + final Random _random = Random(); + + /// The duration for which generated codes are considered valid. + final Duration codeExpiryDuration; + + DbCollection get _collection => + _connectionManager.db.collection(kVerificationCodesCollection); + + /// Initializes the service by ensuring the TTL index exists. + Future _init() async { + try { + _log.info('Ensuring TTL index exists for verification codes...'); + final command = { + 'createIndexes': kVerificationCodesCollection, + 'indexes': [ + { + 'key': {'expiresAt': 1}, + 'name': 'expiresAt_ttl_index', + 'expireAfterSeconds': 0, + } + ] + }; + await _connectionManager.db.runCommand(command); + _log.info('Verification codes TTL index is set up correctly.'); + } catch (e, s) { + _log.severe( + 'Failed to create TTL index for verification codes collection.', + e, + s, + ); + rethrow; + } + } + + String _generateNumericCode({int length = 6}) { + final buffer = StringBuffer(); + for (var i = 0; i < length; i++) { + buffer.write(_random.nextInt(10).toString()); + } + return buffer.toString(); + } + + @override + Future generateAndStoreSignInCode(String email) async { + final code = _generateNumericCode(); + final expiresAt = DateTime.now().add(codeExpiryDuration); + + try { + await _collection.updateOne( + where.eq('_id', email), + modify.set('code', code).set('expiresAt', expiresAt), + upsert: true, + ); + _log.info( + 'Stored sign-in code for $email (expires: $expiresAt)', + ); + return code; + } catch (e) { + _log.severe('Failed to store sign-in code for $email: $e'); + throw OperationFailedException('Failed to store sign-in code: $e'); + } + } + + @override + Future validateSignInCode(String email, String code) async { + try { + final entry = await _collection.findOne(where.id(email)); + if (entry == null) { + return false; // No code found for this email + } + + final storedCode = entry['code'] as String?; + final expiresAt = entry['expiresAt'] as DateTime?; + + if (storedCode != code || + expiresAt == null || + DateTime.now().isAfter(expiresAt)) { + return false; // Code mismatch or expired + } + + return true; + } catch (e) { + _log.severe('Error validating sign-in code for $email: $e'); + throw OperationFailedException('Failed to validate sign-in code: $e'); + } + } + + @override + Future clearSignInCode(String email) async { + try { + await _collection.deleteOne(where.id(email)); + _log.info('Cleared sign-in code for $email'); + } catch (e) { + _log.severe('Failed to clear sign-in code for $email: $e'); + throw OperationFailedException('Failed to clear sign-in code: $e'); + } + } + + @override + Future cleanupExpiredCodes() async { + // No-op, handled by TTL index. + _log.finer( + 'cleanupExpiredCodes() called, but no action is needed due to TTL index.', + ); + return Future.value(); + } + + @override + void dispose() { + // No-op, connection managed by AppDependencies. + _log.finer('dispose() called, no action needed.'); + } +} From 0cb5741d8091e2feee20e48bdcafb6d649f3ff03 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:26:47 +0100 Subject: [PATCH 07/22] refactor(mongodb): Improve verification code storage - Added unique index on email field - Improved code handling and validation - Enhanced logging messages - Removed redundant cleanup method - Updated comments for clarity --- ...odb_verification_code_storage_service.dart | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/src/services/mongodb_verification_code_storage_service.dart b/lib/src/services/mongodb_verification_code_storage_service.dart index 6aec749..9af0a0d 100644 --- a/lib/src/services/mongodb_verification_code_storage_service.dart +++ b/lib/src/services/mongodb_verification_code_storage_service.dart @@ -14,7 +14,8 @@ const String kVerificationCodesCollection = 'verification_codes'; /// A MongoDB-backed implementation of [VerificationCodeStorageService]. /// /// Stores verification codes in a dedicated MongoDB collection with a TTL -/// index on an `expiresAt` field for automatic cleanup. +/// index on an `expiresAt` field for automatic cleanup. It uses a unique +/// index on the `email` field to ensure data integrity. /// {@endtemplate} class MongoDbVerificationCodeStorageService implements VerificationCodeStorageService { @@ -38,25 +39,32 @@ class MongoDbVerificationCodeStorageService DbCollection get _collection => _connectionManager.db.collection(kVerificationCodesCollection); - /// Initializes the service by ensuring the TTL index exists. + /// Initializes the service by ensuring required indexes exist. Future _init() async { try { - _log.info('Ensuring TTL index exists for verification codes...'); + _log.info('Ensuring indexes exist for verification codes...'); final command = { 'createIndexes': kVerificationCodesCollection, 'indexes': [ + // TTL index for automatic document expiration { 'key': {'expiresAt': 1}, 'name': 'expiresAt_ttl_index', 'expireAfterSeconds': 0, + }, + // Unique index to ensure only one code per email + { + 'key': {'email': 1}, + 'name': 'email_unique_index', + 'unique': true, } ] }; await _connectionManager.db.runCommand(command); - _log.info('Verification codes TTL index is set up correctly.'); + _log.info('Verification codes indexes are set up correctly.'); } catch (e, s) { _log.severe( - 'Failed to create TTL index for verification codes collection.', + 'Failed to create indexes for verification codes collection.', e, s, ); @@ -78,9 +86,14 @@ class MongoDbVerificationCodeStorageService final expiresAt = DateTime.now().add(codeExpiryDuration); try { + // Use updateOne with upsert: if a document for the email exists, + // it's updated with a new code and expiry; otherwise, it's created. await _collection.updateOne( - where.eq('_id', email), - modify.set('code', code).set('expiresAt', expiresAt), + where.eq('email', email), + modify + .set('code', code) + .set('expiresAt', expiresAt) + .setOnInsert('_id', ObjectId()), upsert: true, ); _log.info( @@ -96,7 +109,7 @@ class MongoDbVerificationCodeStorageService @override Future validateSignInCode(String email, String code) async { try { - final entry = await _collection.findOne(where.id(email)); + final entry = await _collection.findOne(where.eq('email', email)); if (entry == null) { return false; // No code found for this email } @@ -104,6 +117,8 @@ class MongoDbVerificationCodeStorageService final storedCode = entry['code'] as String?; final expiresAt = entry['expiresAt'] as DateTime?; + // The TTL index handles automatic deletion, but this check prevents + // using a code in the brief window before it's deleted. if (storedCode != code || expiresAt == null || DateTime.now().isAfter(expiresAt)) { @@ -120,7 +135,8 @@ class MongoDbVerificationCodeStorageService @override Future clearSignInCode(String email) async { try { - await _collection.deleteOne(where.id(email)); + // After successful validation, the code should be removed immediately. + await _collection.deleteOne(where.eq('email', email)); _log.info('Cleared sign-in code for $email'); } catch (e) { _log.severe('Failed to clear sign-in code for $email: $e'); @@ -130,7 +146,8 @@ class MongoDbVerificationCodeStorageService @override Future cleanupExpiredCodes() async { - // No-op, handled by TTL index. + // This is a no-op because the TTL index on the MongoDB collection + // handles the cleanup automatically on the server side. _log.finer( 'cleanupExpiredCodes() called, but no action is needed due to TTL index.', ); @@ -139,7 +156,8 @@ class MongoDbVerificationCodeStorageService @override void dispose() { - // No-op, connection managed by AppDependencies. + // This is a no-op because the underlying database connection is managed + // by the injected MongoDbConnectionManager. _log.finer('dispose() called, no action needed.'); } } From 8e74597a386b26a13fc51b8df12742392040d93d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:27:10 +0100 Subject: [PATCH 08/22] refactor(services): Replace in-memory verification code storage - Replaced InMemoryVerificationCodeStorageService - with MongoDbVerificationCodeStorageService - Improved persistence and scalability - Added logging to new service - Updated AppDependencies to use new service --- lib/src/config/app_dependencies.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index d41c28f..f9bddf9 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -7,6 +7,7 @@ import 'package:ht_api/src/services/database_seeding_service.dart'; import 'package:ht_api/src/services/default_user_preference_limit_service.dart'; import 'package:ht_api/src/services/jwt_auth_token_service.dart'; import 'package:ht_api/src/services/mongodb_token_blacklist_service.dart'; +import 'package:ht_api/src/services/mongodb_verification_code_storage_service.dart'; import 'package:ht_api/src/services/token_blacklist_service.dart'; import 'package:ht_api/src/services/user_preference_limit_service.dart'; import 'package:ht_api/src/services/verification_code_storage_service.dart'; @@ -179,7 +180,10 @@ class AppDependencies { blacklistService: tokenBlacklistService, log: Logger('JwtAuthTokenService'), ); - verificationCodeStorageService = InMemoryVerificationCodeStorageService(); + verificationCodeStorageService = MongoDbVerificationCodeStorageService( + connectionManager: _mongoDbConnectionManager, + log: Logger('MongoDbVerificationCodeStorageService'), + ); permissionService = const PermissionService(); authService = AuthService( userRepository: userRepository, From 1550783ffec53fb332155eae04840cd82365fadb Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:33:09 +0100 Subject: [PATCH 09/22] refactor(services): remove unnecessary initialization - Removed redundant `_init` method. - Simplified service instantiation. - Improved code readability. --- .../mongodb_token_blacklist_service.dart | 36 +------------------ 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/lib/src/services/mongodb_token_blacklist_service.dart b/lib/src/services/mongodb_token_blacklist_service.dart index a2c3d29..3974b7b 100644 --- a/lib/src/services/mongodb_token_blacklist_service.dart +++ b/lib/src/services/mongodb_token_blacklist_service.dart @@ -22,10 +22,7 @@ class MongoDbTokenBlacklistService implements TokenBlacklistService { required MongoDbConnectionManager connectionManager, required Logger log, }) : _connectionManager = connectionManager, - _log = log { - // Fire-and-forget initialization. Errors are logged internally. - _init(); - } + _log = log; final MongoDbConnectionManager _connectionManager; final Logger _log; @@ -33,37 +30,6 @@ class MongoDbTokenBlacklistService implements TokenBlacklistService { DbCollection get _collection => _connectionManager.db.collection(kBlacklistedTokensCollection); - /// Initializes the service by ensuring the TTL index exists. - /// This is idempotent and safe to call multiple times. - Future _init() async { - try { - _log.info('Ensuring TTL index exists for blacklist collection...'); - // Using a raw command is more robust against client library changes. - final command = { - 'createIndexes': kBlacklistedTokensCollection, - 'indexes': [ - { - 'key': {'expiry': 1}, - 'name': 'expiry_ttl_index', - 'expireAfterSeconds': 0, - } - ] - }; - await _connectionManager.db.runCommand(command); - _log.info('Blacklist TTL index is set up correctly.'); - } catch (e, s) { - _log.severe( - 'Failed to create TTL index for blacklist collection. ' - 'Failed to create TTL index for blacklist collection. ' - 'Automatic cleanup of expired tokens will not work.', - e, - s, - ); - // Rethrow the exception to halt startup if the index is critical. - rethrow; - } - } - @override Future blacklist(String jti, DateTime expiry) async { try { From eb04f61108b95d4d3e33474cab13109992ce1ebf Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:33:41 +0100 Subject: [PATCH 10/22] refactor(services): remove unnecessary init method - Removed `_init` method. - Simplified service initialization. - Improved code readability. --- ...odb_verification_code_storage_service.dart | 37 +------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/lib/src/services/mongodb_verification_code_storage_service.dart b/lib/src/services/mongodb_verification_code_storage_service.dart index 9af0a0d..e4436f5 100644 --- a/lib/src/services/mongodb_verification_code_storage_service.dart +++ b/lib/src/services/mongodb_verification_code_storage_service.dart @@ -25,9 +25,7 @@ class MongoDbVerificationCodeStorageService required Logger log, this.codeExpiryDuration = const Duration(minutes: 15), }) : _connectionManager = connectionManager, - _log = log { - _init(); - } + _log = log; final MongoDbConnectionManager _connectionManager; final Logger _log; @@ -39,39 +37,6 @@ class MongoDbVerificationCodeStorageService DbCollection get _collection => _connectionManager.db.collection(kVerificationCodesCollection); - /// Initializes the service by ensuring required indexes exist. - Future _init() async { - try { - _log.info('Ensuring indexes exist for verification codes...'); - final command = { - 'createIndexes': kVerificationCodesCollection, - 'indexes': [ - // TTL index for automatic document expiration - { - 'key': {'expiresAt': 1}, - 'name': 'expiresAt_ttl_index', - 'expireAfterSeconds': 0, - }, - // Unique index to ensure only one code per email - { - 'key': {'email': 1}, - 'name': 'email_unique_index', - 'unique': true, - } - ] - }; - await _connectionManager.db.runCommand(command); - _log.info('Verification codes indexes are set up correctly.'); - } catch (e, s) { - _log.severe( - 'Failed to create indexes for verification codes collection.', - e, - s, - ); - rethrow; - } - } - String _generateNumericCode({int length = 6}) { final buffer = StringBuffer(); for (var i = 0; i < length; i++) { From 8e52d814f06f33b8803648d01e06bfc7ab10d491 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:34:29 +0100 Subject: [PATCH 11/22] feat(database): add indexes to verification codes and tokens - Added TTL index for verification codes expiry. - Added unique index for verification codes email. - Added TTL index for blacklisted tokens expiry. --- .../services/database_seeding_service.dart | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/lib/src/services/database_seeding_service.dart b/lib/src/services/database_seeding_service.dart index cf91e5e..8a8bf1a 100644 --- a/lib/src/services/database_seeding_service.dart +++ b/lib/src/services/database_seeding_service.dart @@ -1,3 +1,5 @@ +import 'package:ht_api/src/services/mongodb_token_blacklist_service.dart'; +import 'package:ht_api/src/services/mongodb_verification_code_storage_service.dart'; import 'package:ht_shared/ht_shared.dart'; import 'package:logging/logging.dart'; import 'package:mongo_dart/mongo_dart.dart'; @@ -143,6 +145,35 @@ class DatabaseSeedingService { name: 'sources_text_index', ); + // Indexes for the verification codes collection + await _db.runCommand({ + 'createIndexes': kVerificationCodesCollection, + 'indexes': [ + { + 'key': {'expiresAt': 1}, + 'name': 'expiresAt_ttl_index', + 'expireAfterSeconds': 0, + }, + { + 'key': {'email': 1}, + 'name': 'email_unique_index', + 'unique': true, + } + ] + }); + + // Index for the token blacklist collection + await _db.runCommand({ + 'createIndexes': kBlacklistedTokensCollection, + 'indexes': [ + { + 'key': {'expiry': 1}, + 'name': 'expiry_ttl_index', + 'expireAfterSeconds': 0, + } + ] + }); + _log.info('Database indexes are set up correctly.'); } on Exception catch (e, s) { _log.severe('Failed to create database indexes.', e, s); From 48cbcfe311411daa090a8c523fa35acf88bf5ddd Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:37:12 +0100 Subject: [PATCH 12/22] refactor(auth): replace print statements with logging - Replaced `print` statements with `Logger`. - Improved logging for better debugging. - Added error handling for token validation. - Used finer logging for detailed info. - Improved logging messages clarity. --- .../authentication_middleware.dart | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/lib/src/middlewares/authentication_middleware.dart b/lib/src/middlewares/authentication_middleware.dart index d479126..d2715a3 100644 --- a/lib/src/middlewares/authentication_middleware.dart +++ b/lib/src/middlewares/authentication_middleware.dart @@ -1,6 +1,9 @@ import 'package:dart_frog/dart_frog.dart'; import 'package:ht_api/src/services/auth_token_service.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger('AuthMiddleware'); /// Middleware to handle authentication by verifying Bearer tokens. /// @@ -17,43 +20,41 @@ import 'package:ht_shared/ht_shared.dart'; Middleware authenticationProvider() { return (handler) { return (context) async { - print('[AuthMiddleware] Entered.'); + _log.finer('Entered.'); // Read the interface type AuthTokenService tokenService; try { - print('[AuthMiddleware] Attempting to read AuthTokenService...'); + _log.finer('Attempting to read AuthTokenService...'); tokenService = context.read(); - print('[AuthMiddleware] Successfully read AuthTokenService.'); + _log.finer('Successfully read AuthTokenService.'); } catch (e, s) { - print('[AuthMiddleware] FAILED to read AuthTokenService: $e\n$s'); + _log.severe('FAILED to read AuthTokenService.', e, s); // Re-throw the error to be caught by the main error handler rethrow; } User? user; // Extract the Authorization header - print('[AuthMiddleware] Attempting to read Authorization header...'); + _log.finer('Attempting to read Authorization header...'); final authHeader = context.request.headers['Authorization']; - print('[AuthMiddleware] Authorization header value: $authHeader'); + _log.finer('Authorization header value: $authHeader'); if (authHeader != null && authHeader.startsWith('Bearer ')) { // Extract the token string final token = authHeader.substring(7); // Length of 'Bearer ' - print('[AuthMiddleware] Extracted Bearer token.'); + _log.finer('Extracted Bearer token.'); try { - print('[AuthMiddleware] Attempting to validate token...'); + _log.finer('Attempting to validate token...'); // Validate the token using the service user = await tokenService.validateToken(token); - print( - '[AuthMiddleware] Token validation returned: ${user?.id ?? 'null'}', + _log.finer( + 'Token validation returned: ${user?.id ?? 'null'}', ); if (user != null) { - print( - '[AuthMiddleware] Authentication successful for user: ${user.id}', - ); + _log.info('Authentication successful for user: ${user.id}'); } else { - print( - '[AuthMiddleware] Invalid token provided (validateToken returned null).', + _log.warning( + 'Invalid token provided (validateToken returned null).', ); // Optional: Could throw UnauthorizedException here if *all* routes // using this middleware strictly require a valid token. @@ -61,25 +62,27 @@ Middleware authenticationProvider() { } } on HtHttpException catch (e) { // Log token validation errors from the service - print('Token validation failed: $e'); + _log.warning('Token validation failed.', e); // Let the error propagate if needed, or handle specific cases. // For now, we treat validation errors as resulting in no user. user = null; // Keep user null if HtHttpException occurred } catch (e, s) { // Catch unexpected errors during validation - print( - '[AuthMiddleware] Unexpected error during token validation: $e\n$s', + _log.severe( + 'Unexpected error during token validation.', + e, + s, ); user = null; // Keep user null if unexpected error occurred } } else { - print('[AuthMiddleware] No valid Bearer token found in header.'); + _log.finer('No valid Bearer token found in header.'); } // Provide the User object (or null) into the context // This makes `context.read()` available downstream. - print( - '[AuthMiddleware] Providing User (${user?.id ?? 'null'}) to context.', + _log.finer( + 'Providing User (${user?.id ?? 'null'}) to context.', ); return handler(context.provide(() => user)); }; @@ -96,14 +99,14 @@ Middleware requireAuthentication() { return (context) { final user = context.read(); if (user == null) { - print( + _log.warning( 'Authentication required but no valid user found. Denying access.', ); // Throwing allows the central errorHandler to create the 401 response. throw const UnauthorizedException('Authentication required.'); } // If user exists, proceed to the handler - print('Authentication check passed for user: ${user.id}'); + _log.info('Authentication check passed for user: ${user.id}'); return handler(context.provide(() => user)); }; }; From 76589a3f1ff12009dd72fa234ab95341a4c89856 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:44:30 +0100 Subject: [PATCH 13/22] fix(authorization): replace print statements with logger - Replaced `print` statements with `Logger` calls. - Improved error handling and logging. - Enhanced debugging capabilities. - Used `Logger.severe` for configuration errors. - Used `Logger.warning` for unsupported actions. --- lib/src/middlewares/authorization_middleware.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/src/middlewares/authorization_middleware.dart b/lib/src/middlewares/authorization_middleware.dart index 7d4d24d..7681e8b 100644 --- a/lib/src/middlewares/authorization_middleware.dart +++ b/lib/src/middlewares/authorization_middleware.dart @@ -2,6 +2,9 @@ import 'package:dart_frog/dart_frog.dart'; import 'package:ht_api/src/rbac/permission_service.dart'; import 'package:ht_api/src/registry/model_registry.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger('AuthorizationMiddleware'); /// {@template authorization_middleware} /// Middleware to enforce role-based permissions and model-specific access rules. @@ -81,9 +84,9 @@ Middleware authorizationMiddleware() { final permission = requiredPermissionConfig.permission; if (permission == null) { // This indicates a configuration error in ModelRegistry - print( - '[AuthorizationMiddleware] Configuration Error: specificPermission ' - 'type requires a permission string for model "$modelName", method "$method".', + _log.severe( + 'Configuration Error: specificPermission type requires a ' + 'permission string for model "$modelName", method "$method".', ); throw const OperationFailedException( 'Internal Server Error: Authorization configuration error.', @@ -97,9 +100,9 @@ Middleware authorizationMiddleware() { case RequiredPermissionType.unsupported: // This action is explicitly marked as not supported via this generic route. // Return Method Not Allowed. - print( - '[AuthorizationMiddleware] Action for model "$modelName", method "$method" ' - 'is marked as unsupported via generic route.', + _log.warning( + 'Action for model "$modelName", method "$method" is marked as ' + 'unsupported via generic route.', ); // Throw ForbiddenException to be caught by the errorHandler throw ForbiddenException( From 73492208cb6e72faa8196c7596e7ef8feac911cc Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:44:55 +0100 Subject: [PATCH 14/22] fix(error_handler): use logger instead of print - Replaced print statements with logger - Improved error logging with stack traces - Added logging package dependency - Used different log levels for severity - Improved error handling clarity --- lib/src/middlewares/error_handler.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/src/middlewares/error_handler.dart b/lib/src/middlewares/error_handler.dart index f1b250b..6e5cae1 100644 --- a/lib/src/middlewares/error_handler.dart +++ b/lib/src/middlewares/error_handler.dart @@ -7,6 +7,9 @@ import 'package:dart_frog/dart_frog.dart'; import 'package:ht_api/src/config/environment_config.dart'; import 'package:ht_shared/ht_shared.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger('ErrorHandler'); /// Middleware that catches errors and converts them into /// standardized JSON responses. @@ -20,7 +23,7 @@ Middleware errorHandler() { } on HtHttpException catch (e, stackTrace) { // Handle specific HtHttpExceptions from the client/repository layers final statusCode = _mapExceptionToStatusCode(e); - print('HtHttpException Caught: $e\n$stackTrace'); // Log for debugging + _log.warning('HtHttpException Caught', e, stackTrace); return _jsonErrorResponse( statusCode: statusCode, exception: e, @@ -32,7 +35,7 @@ Middleware errorHandler() { final message = 'Invalid request body: Field "$field" has an ' 'invalid value or is missing. ${e.message}'; - print('CheckedFromJsonException Caught: $e\n$stackTrace'); + _log.warning('CheckedFromJsonException Caught', e, stackTrace); return _jsonErrorResponse( statusCode: HttpStatus.badRequest, // 400 exception: InvalidInputException(message), @@ -40,7 +43,7 @@ Middleware errorHandler() { ); } on FormatException catch (e, stackTrace) { // Handle data format/parsing errors (often indicates bad client input) - print('FormatException Caught: $e\n$stackTrace'); // Log for debugging + _log.warning('FormatException Caught', e, stackTrace); return _jsonErrorResponse( statusCode: HttpStatus.badRequest, // 400 exception: InvalidInputException('Invalid data format: ${e.message}'), @@ -48,7 +51,7 @@ Middleware errorHandler() { ); } catch (e, stackTrace) { // Handle any other unexpected errors - print('Unhandled Exception Caught: $e\n$stackTrace'); + _log.severe('Unhandled Exception Caught', e, stackTrace); return _jsonErrorResponse( statusCode: HttpStatus.internalServerError, // 500 exception: const UnknownException( From 2ea1f83baa17588f26e17f7f912eedc3ff7734c0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:45:24 +0100 Subject: [PATCH 15/22] refactor: remove unused auth & verification services - Removed `SimpleAuthTokenService`. - Removed `InMemoryVerificationCodeStorageService`. --- .../services/simple_auth_token_service.dart | 88 -------------- .../verification_code_storage_service.dart | 115 ------------------ 2 files changed, 203 deletions(-) delete mode 100644 lib/src/services/simple_auth_token_service.dart diff --git a/lib/src/services/simple_auth_token_service.dart b/lib/src/services/simple_auth_token_service.dart deleted file mode 100644 index 618a29a..0000000 --- a/lib/src/services/simple_auth_token_service.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:ht_api/src/services/auth_token_service.dart'; -import 'package:ht_data_repository/ht_data_repository.dart'; -import 'package:ht_shared/ht_shared.dart'; -import 'package:logging/logging.dart'; - -/// {@template simple_auth_token_service} -/// A minimal, dependency-free implementation of [AuthTokenService] for debugging. -/// -/// Generates simple, predictable tokens and validates them by checking a prefix -/// and fetching the user from the repository. Does not involve JWT logic. -/// {@endtemplate} -class SimpleAuthTokenService implements AuthTokenService { - /// {@macro simple_auth_token_service} - const SimpleAuthTokenService({ - required HtDataRepository userRepository, - required Logger log, - }) : _userRepository = userRepository, - _log = log; - - final HtDataRepository _userRepository; - final Logger _log; - static const String _tokenPrefix = 'valid-token-for-user-id:'; - - @override - Future generateToken(User user) async { - _log.info('Generating token for user ${user.id}'); - final token = '$_tokenPrefix${user.id}'; - _log.finer('Generated token: $token'); - // Simulate async operation if needed, though not strictly necessary here - await Future.delayed(Duration.zero); - return token; - } - - @override - Future validateToken(String token) async { - _log.finer('Attempting to validate token: $token'); - if (!token.startsWith(_tokenPrefix)) { - _log.warning('Validation failed: Invalid prefix.'); - // Mimic JWT behavior by throwing Unauthorized for invalid format - throw const UnauthorizedException('Invalid token format.'); - } - - final userId = token.substring(_tokenPrefix.length); - _log.finer('Extracted user ID: $userId'); - - if (userId.isEmpty) { - _log.warning('Validation failed: Empty user ID.'); - throw const UnauthorizedException('Invalid token: Empty user ID.'); - } - - try { - _log.finer('Attempting to read user from repository...'); - final user = await _userRepository.read(id: userId); - _log.info('User read successful: ${user.id}'); - return user; - } on NotFoundException { - _log.warning('Validation failed: User ID $userId not found.'); - // Return null if user not found, mimicking successful validation - // of a token for a non-existent user. The middleware handles this. - return null; - } on HtHttpException catch (e, s) { - // Handle other potential repository errors - _log.warning('Validation failed: Repository error', e, s); - // Re-throw other client/repo exceptions - rethrow; - } catch (e, s) { - // Catch unexpected errors during validation - _log.severe('Unexpected validation error', e, s); - throw OperationFailedException( - 'Simple token validation failed unexpectedly: $e', - ); - } - } - - @override - Future invalidateToken(String token) async { - // This service uses simple prefixed tokens, not JWTs with JTI. - // True invalidation/blacklisting isn't applicable here. - // This method is implemented to satisfy the AuthTokenService interface. - _log.info( - 'Received request to invalidate token: $token. ' - 'No server-side invalidation is performed for simple tokens.', - ); - // Simulate async operation - await Future.delayed(Duration.zero); - // No specific exceptions thrown here. - } -} diff --git a/lib/src/services/verification_code_storage_service.dart b/lib/src/services/verification_code_storage_service.dart index ef21f89..a832009 100644 --- a/lib/src/services/verification_code_storage_service.dart +++ b/lib/src/services/verification_code_storage_service.dart @@ -67,118 +67,3 @@ abstract class VerificationCodeStorageService { /// Disposes of any resources used by the service (e.g., timers for cleanup). void dispose(); } - -/// {@template in_memory_verification_code_storage_service} -/// An in-memory implementation of [VerificationCodeStorageService]. -/// -/// Stores verification codes in memory. Not suitable for production if -/// persistence across server restarts is required. -/// {@endtemplate} -class InMemoryVerificationCodeStorageService - implements VerificationCodeStorageService { - /// {@macro in_memory_verification_code_storage_service} - InMemoryVerificationCodeStorageService({ - Duration cleanupInterval = _defaultCleanupInterval, - this.codeExpiryDuration = _defaultCodeExpiryDuration, - }) { - _cleanupTimer = Timer.periodic(cleanupInterval, (_) async { - try { - await cleanupExpiredCodes(); - } catch (e) { - print( - '[InMemoryVerificationCodeStorageService] Error during scheduled cleanup: $e', - ); - } - }); - print( - '[InMemoryVerificationCodeStorageService] Initialized with cleanup interval: ' - '$cleanupInterval and code expiry: $codeExpiryDuration', - ); - } - - /// Duration for which generated codes are considered valid. - final Duration codeExpiryDuration; - - /// Store for standard sign-in codes: Key is email. - @visibleForTesting - final Map signInCodesStore = {}; - - Timer? _cleanupTimer; - bool _isDisposed = false; - final Random _random = Random(); - - String _generateNumericCode({int length = 6}) { - final buffer = StringBuffer(); - for (var i = 0; i < length; i++) { - buffer.write(_random.nextInt(10).toString()); - } - return buffer.toString(); - } - - @override - Future generateAndStoreSignInCode(String email) async { - if (_isDisposed) { - throw const OperationFailedException('Service is disposed.'); - } - await Future.delayed(Duration.zero); // Simulate async - final code = _generateNumericCode(); - final expiresAt = DateTime.now().add(codeExpiryDuration); - signInCodesStore[email] = _SignInCodeEntry(code, expiresAt); - print( - '[InMemoryVerificationCodeStorageService] Stored sign-in code: $code for $email (expires: $expiresAt)', - ); - return code; - } - - @override - Future validateSignInCode(String email, String code) async { - if (_isDisposed) return false; - await Future.delayed(Duration.zero); // Simulate async - final entry = signInCodesStore[email]; - if (entry == null || entry.isExpired || entry.code != code) { - return false; - } - return true; - } - - @override - Future clearSignInCode(String email) async { - if (_isDisposed) return; - await Future.delayed(Duration.zero); // Simulate async - signInCodesStore.remove(email); - print( - '[InMemoryVerificationCodeStorageService] Cleared sign-in code for $email', - ); - } - - @override - Future cleanupExpiredCodes() async { - if (_isDisposed) return; - await Future.delayed(Duration.zero); // Simulate async - var cleanedCount = 0; - - signInCodesStore.removeWhere((key, entry) { - if (entry.isExpired) { - cleanedCount++; - return true; - } - return false; - }); - - if (cleanedCount > 0) { - print( - '[InMemoryVerificationCodeStorageService] Cleaned up $cleanedCount expired codes.', - ); - } - } - - @override - void dispose() { - if (!_isDisposed) { - _isDisposed = true; - _cleanupTimer?.cancel(); - signInCodesStore.clear(); - print('[InMemoryVerificationCodeStorageService] Disposed.'); - } - } -} From 74935bfe75b18a710e884c3c5c08c67dd779a4f3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:54:39 +0100 Subject: [PATCH 16/22] fix(auth): improve anonymous auth error handling - Added logging for unexpected errors - Improved error handling for exceptions - Replaced print statement with logger --- routes/api/v1/auth/anonymous.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/routes/api/v1/auth/anonymous.dart b/routes/api/v1/auth/anonymous.dart index 7827b36..8007d7a 100644 --- a/routes/api/v1/auth/anonymous.dart +++ b/routes/api/v1/auth/anonymous.dart @@ -4,6 +4,10 @@ import 'package:dart_frog/dart_frog.dart'; import 'package:ht_api/src/helpers/response_helper.dart'; import 'package:ht_api/src/services/auth_service.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:logging/logging.dart'; + +// Create a logger for this file. +final _logger = Logger('anonymous_handler'); /// Handles POST requests to `/api/v1/auth/anonymous`. /// @@ -37,9 +41,9 @@ Future onRequest(RequestContext context) async { } on HtHttpException catch (_) { // Let the central errorHandler middleware handle known exceptions rethrow; - } catch (e) { + } catch (e, s) { // Catch unexpected errors from the service layer - print('Unexpected error in /anonymous handler: $e'); + _logger.severe('Unexpected error in /anonymous handler', e, s); // Let the central errorHandler handle this as a 500 throw const OperationFailedException( 'An unexpected error occurred during anonymous sign-in.', From 981e36601a838a15d786526b572613cc1eb65cd1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:55:04 +0100 Subject: [PATCH 17/22] fix(auth): improve error handling in delete-account - Added logging for unexpected errors. - Improved error handling for service exceptions. - Replaced print statements with logger. --- routes/api/v1/auth/delete-account.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/routes/api/v1/auth/delete-account.dart b/routes/api/v1/auth/delete-account.dart index 6e7eacc..310e885 100644 --- a/routes/api/v1/auth/delete-account.dart +++ b/routes/api/v1/auth/delete-account.dart @@ -3,6 +3,10 @@ import 'dart:io'; import 'package:dart_frog/dart_frog.dart'; import 'package:ht_api/src/services/auth_service.dart'; import 'package:ht_shared/ht_shared.dart'; // For User and exceptions +import 'package:logging/logging.dart'; + +// Create a logger for this file. +final _logger = Logger('delete_account_handler'); /// Handles DELETE requests to `/api/v1/auth/delete-account`. /// @@ -38,10 +42,12 @@ Future onRequest(RequestContext context) async { } on HtHttpException catch (_) { // Let the central errorHandler middleware handle known exceptions rethrow; - } catch (e) { + } catch (e, s) { // Catch unexpected errors from the service layer - print( - 'Unexpected error in /delete-account handler for user ${user.id}: $e', + _logger.severe( + 'Unexpected error in /delete-account handler for user ${user.id}', + e, + s, ); // Let the central errorHandler handle this as a 500 throw const OperationFailedException( From 205acd88dffdfda8650a0a47f5e8d6559a0144e7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:55:31 +0100 Subject: [PATCH 18/22] fix(auth): improve error handling in request-code handler - Added logging for unexpected errors. - Improved error handling using Logger. - Replaced print statements with logging. - Catches and logs stack traces. - Uses HtHttpException for known exceptions. --- routes/api/v1/auth/request-code.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/routes/api/v1/auth/request-code.dart b/routes/api/v1/auth/request-code.dart index 11b6706..f809454 100644 --- a/routes/api/v1/auth/request-code.dart +++ b/routes/api/v1/auth/request-code.dart @@ -3,6 +3,10 @@ import 'dart:io'; import 'package:dart_frog/dart_frog.dart'; import 'package:ht_api/src/services/auth_service.dart'; import 'package:ht_shared/ht_shared.dart'; // For exceptions +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`. /// @@ -79,9 +83,9 @@ Future onRequest(RequestContext context) async { } on HtHttpException catch (_) { // Let the central errorHandler middleware handle known exceptions rethrow; - } catch (e) { + } catch (e, s) { // Catch unexpected errors from the service layer - print('Unexpected error in /request-code handler: $e'); + _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 ee01c859fafe7ff30508d425d26ff1d6d46a88e1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:56:07 +0100 Subject: [PATCH 19/22] fix(auth): improve sign-out error logging - Replaced `print` statements with logger. - Added stack traces to error logs. - Improved error message clarity. - Used structured logging for better analysis. --- routes/api/v1/auth/sign-out.dart | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/routes/api/v1/auth/sign-out.dart b/routes/api/v1/auth/sign-out.dart index 302edcc..47eb73e 100644 --- a/routes/api/v1/auth/sign-out.dart +++ b/routes/api/v1/auth/sign-out.dart @@ -3,6 +3,10 @@ import 'dart:io'; import 'package:dart_frog/dart_frog.dart'; import 'package:ht_api/src/services/auth_service.dart'; import 'package:ht_shared/ht_shared.dart'; // For User and exceptions +import 'package:logging/logging.dart'; + +// Create a logger for this file. +final _logger = Logger('sign_out_handler'); /// Handles POST requests to `/api/v1/auth/sign-out`. /// @@ -34,8 +38,8 @@ Future onRequest(RequestContext context) async { // Although authentication middleware should ensure a token is present, // this check acts as a safeguard. if (token == null || token.isEmpty) { - print( - 'Error: Could not extract Bearer token for user ${user.id} in sign-out handler.', + _logger.severe( + 'Could not extract Bearer token for user ${user.id} in sign-out handler.', ); throw const OperationFailedException( 'Internal error: Unable to retrieve authentication token for sign-out.', @@ -55,9 +59,13 @@ Future onRequest(RequestContext context) async { } on HtHttpException catch (_) { // Let the central errorHandler middleware handle known exceptions rethrow; - } catch (e) { + } catch (e, s) { // Catch unexpected errors from the service layer - print('Unexpected error in /sign-out handler for user ${user.id}: $e'); + _logger.severe( + 'Unexpected error in /sign-out handler for user ${user.id}', + e, + s, + ); // Let the central errorHandler handle this as a 500 throw const OperationFailedException( 'An unexpected error occurred during sign-out.', From 170539f5dbf7fe6aeafabb453116d656271ef49a Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:56:49 +0100 Subject: [PATCH 20/22] fix: improve error logging in verify-code handler - Added logging using the `logging` package. - Log unexpected errors with stack trace. - Improved error message clarity. --- routes/api/v1/auth/verify-code.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/routes/api/v1/auth/verify-code.dart b/routes/api/v1/auth/verify-code.dart index 1d464b6..c4bf7bc 100644 --- a/routes/api/v1/auth/verify-code.dart +++ b/routes/api/v1/auth/verify-code.dart @@ -5,6 +5,10 @@ import 'package:ht_api/src/helpers/response_helper.dart'; import 'package:ht_api/src/services/auth_service.dart'; // Import exceptions, User, SuccessApiResponse, AND AuthSuccessResponse import 'package:ht_shared/ht_shared.dart'; +import 'package:logging/logging.dart'; + +// Create a logger for this file. +final _logger = Logger('verify_code_handler'); /// Handles POST requests to `/api/v1/auth/verify-code`. /// @@ -105,9 +109,9 @@ Future onRequest(RequestContext context) async { // Let the central errorHandler middleware handle known exceptions // (e.g., InvalidInputException if code is wrong/expired) rethrow; - } catch (e) { + } catch (e, s) { // Catch unexpected errors from the service layer - print('Unexpected error in /verify-code handler: $e'); + _logger.severe('Unexpected error in /verify-code handler', e, s); // Let the central errorHandler handle this as a 500 throw const OperationFailedException( 'An unexpected error occurred while verifying the sign-in code.', From 85a289bd9a8e046fcf833ea1901cfb5abc02445a Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 18:59:10 +0100 Subject: [PATCH 21/22] fix(api): replace print statements with logger - Replaced `print` statements with `_logger` - Improved error handling and logging - Added stack traces to error logs - Used more specific log levels - Improved log message clarity --- routes/api/v1/data/[id]/index.dart | 50 +++++++++++++++++------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/routes/api/v1/data/[id]/index.dart b/routes/api/v1/data/[id]/index.dart index b390ebe..05ada7c 100644 --- a/routes/api/v1/data/[id]/index.dart +++ b/routes/api/v1/data/[id]/index.dart @@ -8,6 +8,10 @@ import 'package:ht_api/src/services/dashboard_summary_service.dart'; import 'package:ht_api/src/services/user_preference_limit_service.dart'; // Import UserPreferenceLimitService import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; +import 'package:logging/logging.dart'; + +// Create a logger for this file. +final _logger = Logger('data_item_handler'); /// Handles requests for the /api/v1/data/[id] endpoint. /// Dispatches requests to specific handlers based on the HTTP method. @@ -136,7 +140,7 @@ Future _handleGet( !permissionService.isAdmin(authenticatedUser)) { // Ensure getOwnerId is provided for models requiring ownership check if (modelConfig.getOwnerId == null) { - print( + _logger.severe( 'Configuration Error: Model "$modelName" requires ' 'ownership check for GET item but getOwnerId is not provided.', ); @@ -192,9 +196,9 @@ Future _handlePut( dynamic itemToUpdate; try { itemToUpdate = modelConfig.fromJson(requestBody); - } on TypeError catch (e) { + } on TypeError catch (e, s) { // Catch errors during deserialization (e.g., missing required fields) - print('Deserialization TypeError in PUT /data/[id]: $e'); + _logger.warning('Deserialization TypeError in PUT /data/[id]', e, s); // Throw BadRequestException to be caught by the errorHandler throw const BadRequestException( 'Invalid request body: Missing or invalid required field(s).', @@ -214,7 +218,7 @@ Future _handlePut( } catch (e) { // Ignore if getId throws, means ID might not be in the body, // which is acceptable depending on the model/client. - print('Warning: Could not get ID from PUT body: $e'); + _logger.info('Could not get ID from PUT body: $e'); } // --- Handler-Level Limit Check (for UserContentPreferences PUT) --- @@ -224,7 +228,7 @@ Future _handlePut( try { // Ensure the itemToUpdate is the correct type for the limit service if (itemToUpdate is! UserContentPreferences) { - print( + _logger.severe( 'Type Error: Expected UserContentPreferences ' 'for limit check, but got ${itemToUpdate.runtimeType}.', ); @@ -239,11 +243,13 @@ Future _handlePut( } on HtHttpException { // Propagate known exceptions from the limit service (e.g., ForbiddenException) rethrow; - } catch (e) { + } catch (e, s) { // Catch unexpected errors from the limit service - print( + _logger.severe( 'Unexpected error during limit check for ' - 'UserContentPreferences PUT: $e', + 'UserContentPreferences PUT', + e, + s, ); throw const OperationFailedException( 'An unexpected error occurred during limit check.', @@ -358,7 +364,7 @@ Future _handlePut( !permissionService.isAdmin(authenticatedUser)) { // Ensure getOwnerId is provided for models requiring ownership check if (modelConfig.getOwnerId == null) { - print( + _logger.severe( 'Configuration Error: Model "$modelName" requires ' 'ownership check for PUT but getOwnerId is not provided.', ); @@ -374,7 +380,7 @@ Future _handlePut( if (itemOwnerId != authenticatedUser.id) { // This scenario should ideally not happen if the repository correctly // enforced ownership during the update call when userId was passed. - print( + _logger.warning( 'Ownership check failed AFTER PUT for item $id. ' 'Item owner: $itemOwnerId, User: ${authenticatedUser.id}', ); @@ -424,7 +430,7 @@ Future _handleDelete( !permissionService.isAdmin(authenticatedUser)) { // Ensure getOwnerId is provided for models requiring ownership check if (modelConfig.getOwnerId == null) { - print( + _logger.severe( 'Configuration Error: Model "$modelName" requires ' 'ownership check for DELETE but getOwnerId is not provided.', ); @@ -461,15 +467,15 @@ Future _handleDelete( final repo = context.read>(); itemToDelete = await repo.read( id: id, - userId: userIdForRepoCall, - ); // userId should be null for AppConfig - default: - print( - 'Error: Unsupported model type "$modelName" reached _handleDelete ownership check.', - ); - // Throw an exception to be caught by the errorHandler - throw OperationFailedException( - 'Unsupported model type "$modelName" reached handler.', + userId: userIdForRepoCall, + ); // userId should be null for AppConfig + default: + _logger.severe( + 'Unsupported model type "$modelName" reached _handleDelete ownership check.', + ); + // Throw an exception to be caught by the errorHandler + throw OperationFailedException( + 'Unsupported model type "$modelName" reached handler.', ); } @@ -534,8 +540,8 @@ Future _handleDelete( default: // This case should ideally be caught by the data/_middleware.dart, // but added for safety. - print( - 'Error: Unsupported model type "$modelName" reached _handleDelete.', + _logger.severe( + 'Unsupported model type "$modelName" reached _handleDelete.', ); // Throw an exception to be caught by the errorHandler throw OperationFailedException( From 89e24172aff8d85a31c71979d89bde7b984f39be Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 20 Jul 2025 19:31:55 +0100 Subject: [PATCH 22/22] feat(auth): configure JWT issuer and expiry - Added JWT issuer and expiry config to .env - Updated JWT service to use config values - Added TTL indexes for tokens and codes - Improved database seeding with new indexes - Configured CORS origin in environment --- .env.example | 9 +++++++++ lib/src/config/environment_config.dart | 16 ++++++++++++++++ lib/src/services/database_seeding_service.dart | 13 +++++++++++++ lib/src/services/jwt_auth_token_service.dart | 15 +++++---------- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index 9884b8a..401f60c 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,12 @@ # This allows the client (e.g., the HT Dashboard) to make requests to the API. # For local development, this can be left unset as 'localhost' is allowed by default. # CORS_ALLOWED_ORIGIN="https://your-dashboard.com" + +# REQUIRED FOR PRODUCTION: The issuer URL for JWTs. +# Defaults to 'http://localhost:8080' for local development if not set. +# For production, this MUST be the public URL of your API. +# JWT_ISSUER="https://api.your-domain.com" + +# OPTIONAL: The expiry duration for JWTs in hours. +# Defaults to 1 hour if not set. +# JWT_EXPIRY_HOURS="24" diff --git a/lib/src/config/environment_config.dart b/lib/src/config/environment_config.dart index 26d02ea..55b37ed 100644 --- a/lib/src/config/environment_config.dart +++ b/lib/src/config/environment_config.dart @@ -108,4 +108,20 @@ abstract final class EnvironmentConfig { /// This is used to configure CORS for production environments. /// Returns `null` if the variable is not set. static String? get corsAllowedOrigin => _env['CORS_ALLOWED_ORIGIN']; + + /// Retrieves the JWT issuer URL from the environment. + /// + /// The value is read from the `JWT_ISSUER` environment variable. + /// Defaults to 'http://localhost:8080' if not set. + static String get jwtIssuer => + _env['JWT_ISSUER'] ?? 'http://localhost:8080'; + + /// Retrieves the JWT expiry duration in hours from the environment. + /// + /// 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'); + return Duration(hours: hours ?? 1); + } } diff --git a/lib/src/services/database_seeding_service.dart b/lib/src/services/database_seeding_service.dart index 8a8bf1a..7a294e3 100644 --- a/lib/src/services/database_seeding_service.dart +++ b/lib/src/services/database_seeding_service.dart @@ -145,16 +145,27 @@ class DatabaseSeedingService { name: 'sources_text_index', ); + // --- TTL and Unique Indexes via runCommand --- + // The following indexes are created using the generic `runCommand` because + // they require specific options not exposed by the simpler `createIndex` + // helper method in the `mongo_dart` library. + // Specifically, `expireAfterSeconds` is needed for TTL indexes. + // Indexes for the verification codes collection await _db.runCommand({ 'createIndexes': kVerificationCodesCollection, 'indexes': [ { + // This is a TTL (Time-To-Live) index. MongoDB will automatically + // delete documents from this collection when the `expiresAt` field's + // value is older than the specified number of seconds (0). 'key': {'expiresAt': 1}, 'name': 'expiresAt_ttl_index', 'expireAfterSeconds': 0, }, { + // This ensures that each email can only have one pending + // verification code at a time, preventing duplicates. 'key': {'email': 1}, 'name': 'email_unique_index', 'unique': true, @@ -167,6 +178,8 @@ class DatabaseSeedingService { 'createIndexes': kBlacklistedTokensCollection, 'indexes': [ { + // This is a TTL index. MongoDB will automatically delete documents + // (blacklisted tokens) when the `expiry` field's value is past. 'key': {'expiry': 1}, 'name': 'expiry_ttl_index', 'expireAfterSeconds': 0, diff --git a/lib/src/services/jwt_auth_token_service.dart b/lib/src/services/jwt_auth_token_service.dart index 42e1eb5..9fd2f34 100644 --- a/lib/src/services/jwt_auth_token_service.dart +++ b/lib/src/services/jwt_auth_token_service.dart @@ -32,19 +32,14 @@ class JwtAuthTokenService implements AuthTokenService { final TokenBlacklistService _blacklistService; final Logger _log; - // --- Configuration --- - - // Define token issuer and default expiry duration - static const String _issuer = 'http://localhost:8080'; - static const Duration _tokenExpiryDuration = Duration(hours: 1); - // --- Interface Implementation --- @override Future generateToken(User user) async { try { final now = DateTime.now(); - final expiry = now.add(_tokenExpiryDuration); + final expiry = now.add(EnvironmentConfig.jwtExpiryDuration); + final issuer = EnvironmentConfig.jwtIssuer; final jwt = JWT( { @@ -52,7 +47,7 @@ class JwtAuthTokenService implements AuthTokenService { 'sub': user.id, // Subject (user ID) - REQUIRED 'exp': expiry.millisecondsSinceEpoch ~/ 1000, // Expiration Time 'iat': now.millisecondsSinceEpoch ~/ 1000, // Issued At - 'iss': _issuer, // Issuer + 'iss': issuer, // Issuer 'jti': ObjectId().oid, // JWT ID (for potential blacklisting) // Custom claims (optional, include what's useful) 'email': user.email, // Kept for convenience @@ -60,7 +55,7 @@ class JwtAuthTokenService implements AuthTokenService { 'appRole': user.appRole.name, 'dashboardRole': user.dashboardRole.name, }, - issuer: _issuer, + issuer: issuer, subject: user.id, jwtId: ObjectId().oid, // Re-setting jti here for clarity if needed ); @@ -69,7 +64,7 @@ class JwtAuthTokenService implements AuthTokenService { final token = jwt.sign( SecretKey(EnvironmentConfig.jwtSecretKey), algorithm: JWTAlgorithm.HS256, - expiresIn: _tokenExpiryDuration, // Redundant but safe + expiresIn: EnvironmentConfig.jwtExpiryDuration, // Redundant but safe ); _log.info('Generated JWT for user ${user.id}');