Skip to content

Commit 01a2e5e

Browse files
committed
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.
1 parent 0d5e933 commit 01a2e5e

File tree

1 file changed

+145
-0
lines changed

1 file changed

+145
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import 'dart:async';
2+
import 'dart:math';
3+
4+
import 'package:ht_api/src/services/verification_code_storage_service.dart';
5+
import 'package:ht_data_mongodb/ht_data_mongodb.dart';
6+
import 'package:ht_shared/ht_shared.dart';
7+
import 'package:logging/logging.dart';
8+
import 'package:mongo_dart/mongo_dart.dart';
9+
10+
/// The name of the MongoDB collection for storing verification codes.
11+
const String kVerificationCodesCollection = 'verification_codes';
12+
13+
/// {@template mongodb_verification_code_storage_service}
14+
/// A MongoDB-backed implementation of [VerificationCodeStorageService].
15+
///
16+
/// Stores verification codes in a dedicated MongoDB collection with a TTL
17+
/// index on an `expiresAt` field for automatic cleanup.
18+
/// {@endtemplate}
19+
class MongoDbVerificationCodeStorageService
20+
implements VerificationCodeStorageService {
21+
/// {@macro mongodb_verification_code_storage_service}
22+
MongoDbVerificationCodeStorageService({
23+
required MongoDbConnectionManager connectionManager,
24+
required Logger log,
25+
this.codeExpiryDuration = const Duration(minutes: 15),
26+
}) : _connectionManager = connectionManager,
27+
_log = log {
28+
_init();
29+
}
30+
31+
final MongoDbConnectionManager _connectionManager;
32+
final Logger _log;
33+
final Random _random = Random();
34+
35+
/// The duration for which generated codes are considered valid.
36+
final Duration codeExpiryDuration;
37+
38+
DbCollection get _collection =>
39+
_connectionManager.db.collection(kVerificationCodesCollection);
40+
41+
/// Initializes the service by ensuring the TTL index exists.
42+
Future<void> _init() async {
43+
try {
44+
_log.info('Ensuring TTL index exists for verification codes...');
45+
final command = {
46+
'createIndexes': kVerificationCodesCollection,
47+
'indexes': [
48+
{
49+
'key': {'expiresAt': 1},
50+
'name': 'expiresAt_ttl_index',
51+
'expireAfterSeconds': 0,
52+
}
53+
]
54+
};
55+
await _connectionManager.db.runCommand(command);
56+
_log.info('Verification codes TTL index is set up correctly.');
57+
} catch (e, s) {
58+
_log.severe(
59+
'Failed to create TTL index for verification codes collection.',
60+
e,
61+
s,
62+
);
63+
rethrow;
64+
}
65+
}
66+
67+
String _generateNumericCode({int length = 6}) {
68+
final buffer = StringBuffer();
69+
for (var i = 0; i < length; i++) {
70+
buffer.write(_random.nextInt(10).toString());
71+
}
72+
return buffer.toString();
73+
}
74+
75+
@override
76+
Future<String> generateAndStoreSignInCode(String email) async {
77+
final code = _generateNumericCode();
78+
final expiresAt = DateTime.now().add(codeExpiryDuration);
79+
80+
try {
81+
await _collection.updateOne(
82+
where.eq('_id', email),
83+
modify.set('code', code).set('expiresAt', expiresAt),
84+
upsert: true,
85+
);
86+
_log.info(
87+
'Stored sign-in code for $email (expires: $expiresAt)',
88+
);
89+
return code;
90+
} catch (e) {
91+
_log.severe('Failed to store sign-in code for $email: $e');
92+
throw OperationFailedException('Failed to store sign-in code: $e');
93+
}
94+
}
95+
96+
@override
97+
Future<bool> validateSignInCode(String email, String code) async {
98+
try {
99+
final entry = await _collection.findOne(where.id(email));
100+
if (entry == null) {
101+
return false; // No code found for this email
102+
}
103+
104+
final storedCode = entry['code'] as String?;
105+
final expiresAt = entry['expiresAt'] as DateTime?;
106+
107+
if (storedCode != code ||
108+
expiresAt == null ||
109+
DateTime.now().isAfter(expiresAt)) {
110+
return false; // Code mismatch or expired
111+
}
112+
113+
return true;
114+
} catch (e) {
115+
_log.severe('Error validating sign-in code for $email: $e');
116+
throw OperationFailedException('Failed to validate sign-in code: $e');
117+
}
118+
}
119+
120+
@override
121+
Future<void> clearSignInCode(String email) async {
122+
try {
123+
await _collection.deleteOne(where.id(email));
124+
_log.info('Cleared sign-in code for $email');
125+
} catch (e) {
126+
_log.severe('Failed to clear sign-in code for $email: $e');
127+
throw OperationFailedException('Failed to clear sign-in code: $e');
128+
}
129+
}
130+
131+
@override
132+
Future<void> cleanupExpiredCodes() async {
133+
// No-op, handled by TTL index.
134+
_log.finer(
135+
'cleanupExpiredCodes() called, but no action is needed due to TTL index.',
136+
);
137+
return Future.value();
138+
}
139+
140+
@override
141+
void dispose() {
142+
// No-op, connection managed by AppDependencies.
143+
_log.finer('dispose() called, no action needed.');
144+
}
145+
}

0 commit comments

Comments
 (0)