@@ -14,7 +14,8 @@ const String kVerificationCodesCollection = 'verification_codes';
1414/// A MongoDB-backed implementation of [VerificationCodeStorageService] .
1515///
1616/// Stores verification codes in a dedicated MongoDB collection with a TTL
17- /// index on an `expiresAt` field for automatic cleanup.
17+ /// index on an `expiresAt` field for automatic cleanup. It uses a unique
18+ /// index on the `email` field to ensure data integrity.
1819/// {@endtemplate}
1920class MongoDbVerificationCodeStorageService
2021 implements VerificationCodeStorageService {
@@ -38,25 +39,32 @@ class MongoDbVerificationCodeStorageService
3839 DbCollection get _collection =>
3940 _connectionManager.db.collection (kVerificationCodesCollection);
4041
41- /// Initializes the service by ensuring the TTL index exists .
42+ /// Initializes the service by ensuring required indexes exist .
4243 Future <void > _init () async {
4344 try {
44- _log.info ('Ensuring TTL index exists for verification codes...' );
45+ _log.info ('Ensuring indexes exist for verification codes...' );
4546 final command = {
4647 'createIndexes' : kVerificationCodesCollection,
4748 'indexes' : [
49+ // TTL index for automatic document expiration
4850 {
4951 'key' : {'expiresAt' : 1 },
5052 'name' : 'expiresAt_ttl_index' ,
5153 'expireAfterSeconds' : 0 ,
54+ },
55+ // Unique index to ensure only one code per email
56+ {
57+ 'key' : {'email' : 1 },
58+ 'name' : 'email_unique_index' ,
59+ 'unique' : true ,
5260 }
5361 ]
5462 };
5563 await _connectionManager.db.runCommand (command);
56- _log.info ('Verification codes TTL index is set up correctly.' );
64+ _log.info ('Verification codes indexes are set up correctly.' );
5765 } catch (e, s) {
5866 _log.severe (
59- 'Failed to create TTL index for verification codes collection.' ,
67+ 'Failed to create indexes for verification codes collection.' ,
6068 e,
6169 s,
6270 );
@@ -78,9 +86,14 @@ class MongoDbVerificationCodeStorageService
7886 final expiresAt = DateTime .now ().add (codeExpiryDuration);
7987
8088 try {
89+ // Use updateOne with upsert: if a document for the email exists,
90+ // it's updated with a new code and expiry; otherwise, it's created.
8191 await _collection.updateOne (
82- where.eq ('_id' , email),
83- modify.set ('code' , code).set ('expiresAt' , expiresAt),
92+ where.eq ('email' , email),
93+ modify
94+ .set ('code' , code)
95+ .set ('expiresAt' , expiresAt)
96+ .setOnInsert ('_id' , ObjectId ()),
8497 upsert: true ,
8598 );
8699 _log.info (
@@ -96,14 +109,16 @@ class MongoDbVerificationCodeStorageService
96109 @override
97110 Future <bool > validateSignInCode (String email, String code) async {
98111 try {
99- final entry = await _collection.findOne (where.id ( email));
112+ final entry = await _collection.findOne (where.eq ( 'email' , email));
100113 if (entry == null ) {
101114 return false ; // No code found for this email
102115 }
103116
104117 final storedCode = entry['code' ] as String ? ;
105118 final expiresAt = entry['expiresAt' ] as DateTime ? ;
106119
120+ // The TTL index handles automatic deletion, but this check prevents
121+ // using a code in the brief window before it's deleted.
107122 if (storedCode != code ||
108123 expiresAt == null ||
109124 DateTime .now ().isAfter (expiresAt)) {
@@ -120,7 +135,8 @@ class MongoDbVerificationCodeStorageService
120135 @override
121136 Future <void > clearSignInCode (String email) async {
122137 try {
123- await _collection.deleteOne (where.id (email));
138+ // After successful validation, the code should be removed immediately.
139+ await _collection.deleteOne (where.eq ('email' , email));
124140 _log.info ('Cleared sign-in code for $email ' );
125141 } catch (e) {
126142 _log.severe ('Failed to clear sign-in code for $email : $e ' );
@@ -130,7 +146,8 @@ class MongoDbVerificationCodeStorageService
130146
131147 @override
132148 Future <void > cleanupExpiredCodes () async {
133- // No-op, handled by TTL index.
149+ // This is a no-op because the TTL index on the MongoDB collection
150+ // handles the cleanup automatically on the server side.
134151 _log.finer (
135152 'cleanupExpiredCodes() called, but no action is needed due to TTL index.' ,
136153 );
@@ -139,7 +156,8 @@ class MongoDbVerificationCodeStorageService
139156
140157 @override
141158 void dispose () {
142- // No-op, connection managed by AppDependencies.
159+ // This is a no-op because the underlying database connection is managed
160+ // by the injected MongoDbConnectionManager.
143161 _log.finer ('dispose() called, no action needed.' );
144162 }
145163}
0 commit comments