2424use MongoDB \Collection ;
2525use MongoDB \Database ;
2626use RuntimeException ;
27+ use Throwable ;
2728
2829final class MongoSecurityGuard extends AbstractSecurityGuardDriver
2930{
@@ -38,6 +39,8 @@ public function __construct(
3839 ) {
3940 parent ::__construct ($ adapter , $ strategy );
4041
42+ $ this ->assertConnected ($ adapter );
43+
4144 $ driver = $ adapter ->getDriver ();
4245
4346 if (!$ driver instanceof Database) {
@@ -48,7 +51,57 @@ public function __construct(
4851
4952 $ this ->db = $ driver ;
5053
51- $ this ->ensureIndexes ();
54+ $ this ->assertCollections ();
55+ }
56+
57+ // -------------------------------------------------------------------------
58+ // IntegrationV2 safeguards
59+ // -------------------------------------------------------------------------
60+
61+ private function assertConnected (AdapterInterface $ adapter ): void
62+ {
63+ if (! $ adapter ->isConnected ()) {
64+ throw new RuntimeException ('IntegrationV2 Mongo connection failed. ' );
65+ }
66+ }
67+
68+ private function assertCollections (): void
69+ {
70+ $ required = [$ this ->attempts , $ this ->blocks ];
71+
72+ try {
73+ $ existing = [];
74+
75+ foreach ($ this ->db ->listCollections () as $ collectionInfo ) {
76+ if (method_exists ($ collectionInfo , 'getName ' )) {
77+ $ existing [] = $ collectionInfo ->getName ();
78+ }
79+ }
80+
81+ $ missing = array_values (array_diff ($ required , $ existing ));
82+
83+ if ($ missing !== []) {
84+ throw new RuntimeException ('IntegrationV2 Mongo schema missing. ' );
85+ }
86+ } catch (RuntimeException $ e ) {
87+ throw $ e ;
88+ } catch (Throwable $ e ) {
89+ throw new RuntimeException ('IntegrationV2 Mongo connection failed. ' , 0 , $ e );
90+ }
91+ }
92+
93+ /**
94+ * @template TReturn
95+ * @param callable():TReturn $operation
96+ * @return TReturn
97+ */
98+ private function executeMongo (callable $ operation , string $ context )
99+ {
100+ try {
101+ return $ operation ();
102+ } catch (Throwable $ e ) {
103+ throw new RuntimeException ('IntegrationV2 Mongo connection failed. ' , 0 , $ e );
104+ }
52105 }
53106
54107 // -------------------------------------------------------------------------
@@ -68,47 +121,30 @@ private function col(string $collection): Collection
68121 return $ col ;
69122 }
70123
71- // -------------------------------------------------------------------------
72- // Ensure Indexes
73- // -------------------------------------------------------------------------
74-
75- private function ensureIndexes (): void
76- {
77- // TTL for attempts (1 day)
78- $ this ->col ($ this ->attempts )->createIndex (
79- ['occurred_at ' => 1 ],
80- ['expireAfterSeconds ' => 86400 ]
81- );
82-
83- // TTL for blocks - expires_at > 0
84- $ this ->col ($ this ->blocks )->createIndex (
85- ['expires_at ' => 1 ],
86- ['expireAfterSeconds ' => 0 ]
87- );
88-
89- $ this ->col ($ this ->attempts )->createIndex (['ip ' => 1 , 'subject ' => 1 ]);
90- $ this ->col ($ this ->blocks )->createIndex (['ip ' => 1 , 'subject ' => 1 ]);
91- }
92-
93124 // -------------------------------------------------------------------------
94125 // doRecordFailure()
95126 // -------------------------------------------------------------------------
96127
97128 protected function doRecordFailure (LoginAttemptDTO $ attempt ): int
98129 {
99- $ this ->col ($ this ->attempts )->insertOne ([
100- 'ip ' => $ attempt ->ip ,
101- 'subject ' => $ attempt ->subject ,
102- 'occurred_at ' => $ attempt ->occurredAt ,
103- ]);
104-
105- $ window = $ this ->now () - 3600 ;
106-
107- return $ this ->col ($ this ->attempts )->countDocuments ([
108- 'ip ' => $ attempt ->ip ,
109- 'subject ' => $ attempt ->subject ,
110- 'occurred_at ' => ['$gte ' => $ window ],
111- ]);
130+ return $ this ->executeMongo (
131+ function () use ($ attempt ): int {
132+ $ this ->col ($ this ->attempts )->insertOne ([
133+ 'ip ' => $ attempt ->ip ,
134+ 'subject ' => $ attempt ->subject ,
135+ 'occurred_at ' => $ attempt ->occurredAt ,
136+ ]);
137+
138+ $ window = $ this ->now () - 3600 ;
139+
140+ return $ this ->col ($ this ->attempts )->countDocuments ([
141+ 'ip ' => $ attempt ->ip ,
142+ 'subject ' => $ attempt ->subject ,
143+ 'occurred_at ' => ['$gte ' => $ window ],
144+ ]);
145+ },
146+ 'recordFailure '
147+ );
112148 }
113149
114150 // -------------------------------------------------------------------------
@@ -117,10 +153,15 @@ protected function doRecordFailure(LoginAttemptDTO $attempt): int
117153
118154 protected function doResetAttempts (string $ ip , string $ subject ): void
119155 {
120- $ this ->col ($ this ->attempts )->deleteMany ([
121- 'ip ' => $ ip ,
122- 'subject ' => $ subject ,
123- ]);
156+ $ this ->executeMongo (
157+ function () use ($ ip , $ subject ): void {
158+ $ this ->col ($ this ->attempts )->deleteMany ([
159+ 'ip ' => $ ip ,
160+ 'subject ' => $ subject ,
161+ ]);
162+ },
163+ 'resetAttempts '
164+ );
124165 }
125166
126167 // -------------------------------------------------------------------------
@@ -129,38 +170,43 @@ protected function doResetAttempts(string $ip, string $subject): void
129170
130171 protected function doGetActiveBlock (string $ ip , string $ subject ): ?SecurityBlockDTO
131172 {
132- $ now = $ this ->now ();
133-
134- /** @var array{
135- * type:string,
136- * expires_at:int,
137- * created_at:int
138- * }|null $row
139- */
140- $ row = $ this ->col ($ this ->blocks )->findOne ([
141- 'ip ' => $ ip ,
142- 'subject ' => $ subject ,
143- '$or ' => [
144- ['expires_at ' => 0 ],
145- ['expires_at ' => ['$gt ' => $ now ]],
146- ],
147- ]);
148-
149- if ($ row === null ) {
150- return null ;
151- }
152-
153- $ type = BlockTypeEnum::tryFrom ($ row ['type ' ]);
154- if ($ type === null ) {
155- return null ;
156- }
157-
158- return new SecurityBlockDTO (
159- ip: $ ip ,
160- subject: $ subject ,
161- type: $ type ,
162- expiresAt: (int )$ row ['expires_at ' ],
163- createdAt: (int )$ row ['created_at ' ],
173+ return $ this ->executeMongo (
174+ function () use ($ ip , $ subject ): ?SecurityBlockDTO {
175+ $ now = $ this ->now ();
176+
177+ /** @var array{
178+ * type:string,
179+ * expires_at:int,
180+ * created_at:int
181+ * }|null $row
182+ */
183+ $ row = $ this ->col ($ this ->blocks )->findOne ([
184+ 'ip ' => $ ip ,
185+ 'subject ' => $ subject ,
186+ '$or ' => [
187+ ['expires_at ' => 0 ],
188+ ['expires_at ' => ['$gt ' => $ now ]],
189+ ],
190+ ]);
191+
192+ if ($ row === null ) {
193+ return null ;
194+ }
195+
196+ $ type = BlockTypeEnum::tryFrom ($ row ['type ' ]);
197+ if ($ type === null ) {
198+ return null ;
199+ }
200+
201+ return new SecurityBlockDTO (
202+ ip: $ ip ,
203+ subject: $ subject ,
204+ type: $ type ,
205+ expiresAt: (int )$ row ['expires_at ' ],
206+ createdAt: (int )$ row ['created_at ' ],
207+ );
208+ },
209+ 'getActiveBlock '
164210 );
165211 }
166212
@@ -170,17 +216,22 @@ protected function doGetActiveBlock(string $ip, string $subject): ?SecurityBlock
170216
171217 protected function doGetRemainingBlockSeconds (string $ ip , string $ subject ): ?int
172218 {
173- $ block = $ this ->doGetActiveBlock ($ ip , $ subject );
219+ return $ this ->executeMongo (
220+ function () use ($ ip , $ subject ): ?int {
221+ $ block = $ this ->doGetActiveBlock ($ ip , $ subject );
174222
175- if ($ block === null ) {
176- return null ;
177- }
223+ if ($ block === null ) {
224+ return null ;
225+ }
178226
179- if ($ block ->expiresAt === 0 ) {
180- return null ; // Permanent block
181- }
227+ if ($ block ->expiresAt === 0 ) {
228+ return null ; // Permanent block
229+ }
182230
183- return max (0 , $ block ->expiresAt - $ this ->now ());
231+ return max (0 , $ block ->expiresAt - $ this ->now ());
232+ },
233+ 'getRemainingBlockSeconds '
234+ );
184235 }
185236
186237 // -------------------------------------------------------------------------
@@ -189,16 +240,21 @@ protected function doGetRemainingBlockSeconds(string $ip, string $subject): ?int
189240
190241 protected function doBlock (SecurityBlockDTO $ block ): void
191242 {
192- $ this ->col ($ this ->blocks )->updateOne (
193- ['ip ' => $ block ->ip , 'subject ' => $ block ->subject ],
194- [
195- '$set ' => [
196- 'type ' => $ block ->type ->value ,
197- 'expires_at ' => $ block ->expiresAt ,
198- 'created_at ' => $ block ->createdAt ,
199- ],
200- ],
201- ['upsert ' => true ]
243+ $ this ->executeMongo (
244+ function () use ($ block ): void {
245+ $ this ->col ($ this ->blocks )->updateOne (
246+ ['ip ' => $ block ->ip , 'subject ' => $ block ->subject ],
247+ [
248+ '$set ' => [
249+ 'type ' => $ block ->type ->value ,
250+ 'expires_at ' => $ block ->expiresAt ,
251+ 'created_at ' => $ block ->createdAt ,
252+ ],
253+ ],
254+ ['upsert ' => true ]
255+ );
256+ },
257+ 'block '
202258 );
203259 }
204260
@@ -208,10 +264,15 @@ protected function doBlock(SecurityBlockDTO $block): void
208264
209265 protected function doUnblock (string $ ip , string $ subject ): void
210266 {
211- $ this ->col ($ this ->blocks )->deleteOne ([
212- 'ip ' => $ ip ,
213- 'subject ' => $ subject ,
214- ]);
267+ $ this ->executeMongo (
268+ function () use ($ ip , $ subject ): void {
269+ $ this ->col ($ this ->blocks )->deleteOne ([
270+ 'ip ' => $ ip ,
271+ 'subject ' => $ subject ,
272+ ]);
273+ },
274+ 'unblock '
275+ );
215276 }
216277
217278 // -------------------------------------------------------------------------
@@ -220,14 +281,19 @@ protected function doUnblock(string $ip, string $subject): void
220281
221282 protected function doCleanup (): void
222283 {
223- $ now = $ this ->now ();
224-
225- // Remove expired blocks (except permanent)
226- $ this ->col ($ this ->blocks )->deleteMany ([
227- 'expires_at ' => ['$ne ' => 0 , '$lte ' => $ now ],
228- ]);
229-
230- // Attempts cleaned automatically via TTL index
284+ $ this ->executeMongo (
285+ function (): void {
286+ $ now = $ this ->now ();
287+
288+ // Remove expired blocks (except permanent)
289+ $ this ->col ($ this ->blocks )->deleteMany ([
290+ 'expires_at ' => ['$ne ' => 0 , '$lte ' => $ now ],
291+ ]);
292+
293+ // Attempts cleaned automatically via TTL index
294+ },
295+ 'cleanup '
296+ );
231297 }
232298
233299 // -------------------------------------------------------------------------
@@ -236,12 +302,17 @@ protected function doCleanup(): void
236302
237303 protected function doGetStats (): array
238304 {
239- $ attempts = $ this ->col ($ this ->attempts )->countDocuments ();
240- $ blocks = $ this ->col ($ this ->blocks )->countDocuments ();
241-
242- return [
243- 'attempts ' => $ attempts ,
244- 'blocks ' => $ blocks ,
245- ];
305+ return $ this ->executeMongo (
306+ function (): array {
307+ $ attempts = $ this ->col ($ this ->attempts )->countDocuments ();
308+ $ blocks = $ this ->col ($ this ->blocks )->countDocuments ();
309+
310+ return [
311+ 'attempts ' => $ attempts ,
312+ 'blocks ' => $ blocks ,
313+ ];
314+ },
315+ 'getStats '
316+ );
246317 }
247318}
0 commit comments