@@ -14,7 +14,8 @@ const String kVerificationCodesCollection = 'verification_codes';
14
14
/// A MongoDB-backed implementation of [VerificationCodeStorageService] .
15
15
///
16
16
/// 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.
18
19
/// {@endtemplate}
19
20
class MongoDbVerificationCodeStorageService
20
21
implements VerificationCodeStorageService {
@@ -38,25 +39,32 @@ class MongoDbVerificationCodeStorageService
38
39
DbCollection get _collection =>
39
40
_connectionManager.db.collection (kVerificationCodesCollection);
40
41
41
- /// Initializes the service by ensuring the TTL index exists .
42
+ /// Initializes the service by ensuring required indexes exist .
42
43
Future <void > _init () async {
43
44
try {
44
- _log.info ('Ensuring TTL index exists for verification codes...' );
45
+ _log.info ('Ensuring indexes exist for verification codes...' );
45
46
final command = {
46
47
'createIndexes' : kVerificationCodesCollection,
47
48
'indexes' : [
49
+ // TTL index for automatic document expiration
48
50
{
49
51
'key' : {'expiresAt' : 1 },
50
52
'name' : 'expiresAt_ttl_index' ,
51
53
'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 ,
52
60
}
53
61
]
54
62
};
55
63
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.' );
57
65
} catch (e, s) {
58
66
_log.severe (
59
- 'Failed to create TTL index for verification codes collection.' ,
67
+ 'Failed to create indexes for verification codes collection.' ,
60
68
e,
61
69
s,
62
70
);
@@ -78,9 +86,14 @@ class MongoDbVerificationCodeStorageService
78
86
final expiresAt = DateTime .now ().add (codeExpiryDuration);
79
87
80
88
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.
81
91
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 ()),
84
97
upsert: true ,
85
98
);
86
99
_log.info (
@@ -96,14 +109,16 @@ class MongoDbVerificationCodeStorageService
96
109
@override
97
110
Future <bool > validateSignInCode (String email, String code) async {
98
111
try {
99
- final entry = await _collection.findOne (where.id ( email));
112
+ final entry = await _collection.findOne (where.eq ( 'email' , email));
100
113
if (entry == null ) {
101
114
return false ; // No code found for this email
102
115
}
103
116
104
117
final storedCode = entry['code' ] as String ? ;
105
118
final expiresAt = entry['expiresAt' ] as DateTime ? ;
106
119
120
+ // The TTL index handles automatic deletion, but this check prevents
121
+ // using a code in the brief window before it's deleted.
107
122
if (storedCode != code ||
108
123
expiresAt == null ||
109
124
DateTime .now ().isAfter (expiresAt)) {
@@ -120,7 +135,8 @@ class MongoDbVerificationCodeStorageService
120
135
@override
121
136
Future <void > clearSignInCode (String email) async {
122
137
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));
124
140
_log.info ('Cleared sign-in code for $email ' );
125
141
} catch (e) {
126
142
_log.severe ('Failed to clear sign-in code for $email : $e ' );
@@ -130,7 +146,8 @@ class MongoDbVerificationCodeStorageService
130
146
131
147
@override
132
148
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.
134
151
_log.finer (
135
152
'cleanupExpiredCodes() called, but no action is needed due to TTL index.' ,
136
153
);
@@ -139,7 +156,8 @@ class MongoDbVerificationCodeStorageService
139
156
140
157
@override
141
158
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.
143
161
_log.finer ('dispose() called, no action needed.' );
144
162
}
145
163
}
0 commit comments