@vivinkv28/strapi-2fa-admin-plugin 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,32 +1,25 @@
1
1
  # @vivinkv28/strapi-2fa-admin-plugin
2
2
 
3
- `@vivinkv28/strapi-2fa-admin-plugin` is a Strapi 5 plugin that adds the backend side of an OTP-based admin login flow.
3
+ Strapi 5 plugin for OTP-based admin authentication.
4
4
 
5
- It gives your Strapi project:
5
+ This package adds the backend flow for:
6
6
 
7
- - admin credential validation before OTP
7
+ - admin email/password validation
8
8
  - OTP challenge creation
9
9
  - OTP resend and verification
10
10
  - rate limiting for login, verify, and resend
11
- - OTP delivery through Strapi's email plugin
12
11
  - final Strapi admin session creation only after OTP verification
13
12
 
14
- ## What This Package Is
13
+ ## Important
15
14
 
16
- This package is a **backend/admin-auth plugin**.
15
+ This package does not automatically replace the default Strapi admin login UI.
17
16
 
18
- It does **not** automatically replace the default Strapi admin login UI.
17
+ You need two parts:
19
18
 
20
- To use it in a real project, you need two parts:
19
+ 1. this plugin for the backend OTP logic
20
+ 2. a host-project patch for the Strapi admin login UI
21
21
 
22
- 1. this plugin for the backend OTP/auth logic
23
- 2. an admin login screen integration in your Strapi project that calls the plugin endpoints
24
-
25
- That means this package is ideal if you want:
26
-
27
- - a reusable backend OTP engine
28
- - control over how the admin login UI looks
29
- - a project-level patch/customization for the Strapi admin login page
22
+ The good part is that this npm package now includes the host patch source directly, so you can copy it into your Strapi project.
30
23
 
31
24
  ## Endpoints
32
25
 
@@ -48,9 +41,9 @@ After installation, the plugin exposes:
48
41
  npm install @vivinkv28/strapi-2fa-admin-plugin
49
42
  ```
50
43
 
51
- ## Step 1: Enable The Plugin
44
+ ## Enable The Plugin
52
45
 
53
- In your Strapi project, update `config/plugins.ts`:
46
+ Update `config/plugins.ts`:
54
47
 
55
48
  ```ts
56
49
  import type { Core } from "@strapi/strapi";
@@ -81,19 +74,17 @@ const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Plugin =>
81
74
  export default config;
82
75
  ```
83
76
 
84
- ## Step 2: Configure Email
77
+ ## Email Provider
85
78
 
86
79
  This plugin sends OTP emails through Strapi's email plugin.
87
80
 
88
- Your project must have a working email provider configuration in `config/plugins.ts`.
89
-
90
- If email is not configured, login will fail when OTP delivery is attempted.
81
+ If your email provider is not configured correctly, login will fail when OTP delivery is attempted.
91
82
 
92
- ## Step 3: Make Sure Server/Proxy Settings Are Correct
83
+ ## Recommended Server Settings
93
84
 
94
- If your Strapi app runs behind a proxy, configure `config/server.ts` correctly so admin cookies work as expected.
85
+ If your Strapi app runs behind a proxy, make sure `config/server.ts` is configured correctly so admin cookies work as expected.
95
86
 
96
- Typical example:
87
+ Example:
97
88
 
98
89
  ```ts
99
90
  import type { Core } from "@strapi/strapi";
@@ -111,238 +102,59 @@ const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server =>
111
102
  export default config;
112
103
  ```
113
104
 
114
- ## Step 4: Add Admin Login UI Integration
115
-
116
- This package does not inject the login UI automatically.
117
-
118
- Your Strapi project must customize the admin login flow so it works like this:
105
+ ## Host Patch Files Included In The Package
119
106
 
120
- 1. admin enters email and password
121
- 2. frontend calls `POST /api/admin-2fa/login`
122
- 3. frontend shows OTP screen
123
- 4. admin enters OTP
124
- 5. frontend calls `POST /api/admin-2fa/verify`
125
- 6. optional resend button calls `POST /api/admin-2fa/resend`
126
-
127
- ## Recommended Project Structure For The Admin Patch
128
-
129
- In your Strapi project, keep your admin patch files in your own `scripts` folder:
107
+ This package includes the admin UI patch source under:
130
108
 
131
109
  ```text
132
- your-project/
133
- scripts/
134
- strapi-admin-2fa-patch/
135
- services/
136
- auth.js
137
- auth.mjs
138
- pages/
139
- Auth/
140
- components/
141
- Login.js
142
- Login.mjs
143
- apply-strapi-admin-2fa-patch.js
110
+ host-patch/
111
+ apply-strapi-admin-2fa-patch.js
112
+ strapi-admin-2fa-patch/
113
+ services/
114
+ auth.js
115
+ auth.mjs
116
+ pages/
117
+ Auth/
118
+ components/
119
+ Login.js
120
+ Login.mjs
144
121
  ```
145
122
 
146
- This keeps your admin customizations reproducible and easy to reapply after `npm install`.
147
-
148
- ## Step 5: Patch The Strapi Admin Auth Service
149
-
150
- Create these files in your Strapi project:
123
+ These are the files you can copy into your Strapi project to get the same admin OTP UI pattern.
151
124
 
152
- - `scripts/strapi-admin-2fa-patch/services/auth.js`
153
- - `scripts/strapi-admin-2fa-patch/services/auth.mjs`
125
+ ## What The UI Patch Does
154
126
 
155
- Start from the corresponding Strapi admin auth service file and add the OTP mutations below.
156
-
157
- ### Add these mutations
158
-
159
- ```js
160
- adminLoginWithOtp: builder.mutation({
161
- query: (body) => ({
162
- method: 'POST',
163
- url: '/api/admin-2fa/login',
164
- data: body,
165
- }),
166
- transformResponse(res) {
167
- return res.data;
168
- },
169
- }),
170
-
171
- verifyAdminLoginOtp: builder.mutation({
172
- query: (body) => ({
173
- method: 'POST',
174
- url: '/api/admin-2fa/verify',
175
- data: body,
176
- }),
177
- transformResponse(res) {
178
- return res.data;
179
- },
180
- invalidatesTags: ['Me'],
181
- }),
182
-
183
- resendAdminLoginOtp: builder.mutation({
184
- query: (body) => ({
185
- method: 'POST',
186
- url: '/api/admin-2fa/resend',
187
- data: body,
188
- }),
189
- transformResponse(res) {
190
- return res.data;
191
- },
192
- }),
193
- ```
194
-
195
- ### Export these hooks
196
-
197
- ```js
198
- const {
199
- useAdminLoginWithOtpMutation,
200
- useVerifyAdminLoginOtpMutation,
201
- useResendAdminLoginOtpMutation,
202
- } = authService;
203
- ```
127
+ The bundled host patch changes the Strapi admin login flow to:
204
128
 
205
- ## Step 6: Patch The Strapi Login Screen
129
+ 1. enter admin email and password
130
+ 2. call `POST /api/admin-2fa/login`
131
+ 3. show an OTP screen
132
+ 4. enter the OTP code
133
+ 5. call `POST /api/admin-2fa/verify`
134
+ 6. optionally resend OTP with `POST /api/admin-2fa/resend`
206
135
 
207
- Create these files in your Strapi project:
136
+ The included login patch UI contains:
208
137
 
209
- - `scripts/strapi-admin-2fa-patch/pages/Auth/components/Login.js`
210
- - `scripts/strapi-admin-2fa-patch/pages/Auth/components/Login.mjs`
211
-
212
- This component must replace the normal one-step login with a two-step state:
213
-
214
- - credentials step
138
+ - email and password step
215
139
  - OTP step
216
-
217
- ### Minimum state you need
218
-
219
- ```js
220
- const [otpStep, setOtpStep] = React.useState(null);
221
- const [apiError, setApiError] = React.useState();
222
-
223
- const [adminLoginWithOtp, { isLoading: isLoggingIn }] = useAdminLoginWithOtpMutation();
224
- const [verifyAdminLoginOtp, { isLoading: isVerifyingOtp }] = useVerifyAdminLoginOtpMutation();
225
- const [resendAdminLoginOtp, { isLoading: isResendingOtp }] = useResendAdminLoginOtpMutation();
226
- ```
227
-
228
- ### Credentials submit handler
229
-
230
- ```js
231
- const handleLogin = async (body) => {
232
- setApiError(undefined);
233
-
234
- const res = await adminLoginWithOtp({
235
- ...body,
236
- deviceId: crypto.randomUUID(),
237
- });
238
-
239
- if ('error' in res) {
240
- setApiError(res.error.message ?? 'Something went wrong');
241
- return;
242
- }
243
-
244
- setOtpStep({
245
- challengeId: res.data.challengeId,
246
- expiresAt: res.data.expiresAt,
247
- maskedEmail: res.data.maskedEmail,
248
- rememberMe: body.rememberMe,
249
- });
250
- };
251
- ```
252
-
253
- ### OTP verify handler
254
-
255
- ```js
256
- const handleVerifyOtp = async ({ code }) => {
257
- if (!otpStep) return;
258
-
259
- setApiError(undefined);
260
-
261
- const res = await verifyAdminLoginOtp({
262
- challengeId: otpStep.challengeId,
263
- code,
264
- });
265
-
266
- if ('error' in res) {
267
- setApiError(res.error.message ?? 'Something went wrong');
268
- return;
269
- }
270
-
271
- dispatch(
272
- login({
273
- token: res.data.token,
274
- persist: otpStep.rememberMe,
275
- })
276
- );
277
-
278
- navigate('/');
279
- };
280
- ```
281
-
282
- ### OTP resend handler
283
-
284
- ```js
285
- const handleResendOtp = async () => {
286
- if (!otpStep) return;
287
-
288
- setApiError(undefined);
289
-
290
- const res = await resendAdminLoginOtp({
291
- challengeId: otpStep.challengeId,
292
- });
293
-
294
- if ('error' in res) {
295
- setApiError(res.error.message ?? 'Something went wrong');
296
- return;
297
- }
298
-
299
- setOtpStep((current) =>
300
- current
301
- ? {
302
- ...current,
303
- expiresAt: res.data.expiresAt,
304
- maskedEmail: res.data.maskedEmail,
305
- }
306
- : current
307
- );
308
- };
309
- ```
310
-
311
- ### OTP input handling
312
-
313
- At minimum:
314
-
315
- ```js
316
- const OTP_LENGTH = 6;
317
- const sanitizeOtp = (value = '') => value.replace(/\D/g, '').slice(0, OTP_LENGTH);
318
- ```
319
-
320
- The working implementation used in the companion project includes:
321
-
322
- - 6 digit boxes
140
+ - 6-digit OTP box UI
323
141
  - paste support
324
- - backspace handling
325
- - auto focus
326
- - inline error state
327
-
328
- ## Step 7: Add A Patch Apply Script
329
-
330
- Create:
331
-
332
- - `scripts/apply-strapi-admin-2fa-patch.js`
142
+ - backspace and focus handling
143
+ - resend OTP button
144
+ - back button
145
+ - inline error handling
333
146
 
334
- This script should:
147
+ ## How To Use The Included Host Patch
335
148
 
336
- 1. copy `scripts/strapi-admin-2fa-patch/services/auth.js`
337
- 2. copy `scripts/strapi-admin-2fa-patch/services/auth.mjs`
338
- 3. copy `scripts/strapi-admin-2fa-patch/pages/Auth/components/Login.js`
339
- 4. copy `scripts/strapi-admin-2fa-patch/pages/Auth/components/Login.mjs`
340
- 5. overwrite the matching files in `node_modules/@strapi/admin/...`
341
- 6. clear stale Strapi admin cache directories
149
+ Copy these files from the package into your Strapi project:
342
150
 
343
- ## Step 8: Wire The Patch Script In `package.json`
151
+ - `host-patch/apply-strapi-admin-2fa-patch.js` -> `scripts/apply-strapi-admin-2fa-patch.js`
152
+ - `host-patch/strapi-admin-2fa-patch/services/auth.js` -> `scripts/strapi-admin-2fa-patch/services/auth.js`
153
+ - `host-patch/strapi-admin-2fa-patch/services/auth.mjs` -> `scripts/strapi-admin-2fa-patch/services/auth.mjs`
154
+ - `host-patch/strapi-admin-2fa-patch/pages/Auth/components/Login.js` -> `scripts/strapi-admin-2fa-patch/pages/Auth/components/Login.js`
155
+ - `host-patch/strapi-admin-2fa-patch/pages/Auth/components/Login.mjs` -> `scripts/strapi-admin-2fa-patch/pages/Auth/components/Login.mjs`
344
156
 
345
- In your Strapi project `package.json`, add:
157
+ Then wire the patch script in your Strapi project's `package.json`:
346
158
 
347
159
  ```json
348
160
  {
@@ -355,12 +167,6 @@ In your Strapi project `package.json`, add:
355
167
  }
356
168
  ```
357
169
 
358
- This ensures the login patch is reapplied:
359
-
360
- - after dependency install
361
- - before build
362
- - before dev
363
-
364
170
  ## Request Flow
365
171
 
366
172
  ### Login
@@ -370,8 +176,6 @@ POST /api/admin-2fa/login
370
176
  Content-Type: application/json
371
177
  ```
372
178
 
373
- Example body:
374
-
375
179
  ```json
376
180
  {
377
181
  "email": "admin@example.com",
@@ -381,19 +185,6 @@ Example body:
381
185
  }
382
186
  ```
383
187
 
384
- Example response:
385
-
386
- ```json
387
- {
388
- "data": {
389
- "challengeId": "***",
390
- "expiresAt": "2026-04-05T18:30:00.000Z",
391
- "maskedEmail": "admin@example.com",
392
- "rememberMe": true
393
- }
394
- }
395
- ```
396
-
397
188
  ### Verify
398
189
 
399
190
  ```http
@@ -401,8 +192,6 @@ POST /api/admin-2fa/verify
401
192
  Content-Type: application/json
402
193
  ```
403
194
 
404
- Example body:
405
-
406
195
  ```json
407
196
  {
408
197
  "challengeId": "***",
@@ -417,8 +206,6 @@ POST /api/admin-2fa/resend
417
206
  Content-Type: application/json
418
207
  ```
419
208
 
420
- Example body:
421
-
422
209
  ```json
423
210
  {
424
211
  "challengeId": "***"
@@ -444,19 +231,17 @@ ADMIN_OTP_RESEND_EMAIL_LIMIT=5
444
231
  ADMIN_OTP_DEBUG_TIMINGS=false
445
232
  ```
446
233
 
447
- ## Testing Checklist
234
+ ## Test Checklist
448
235
 
449
- After setup, test these cases:
236
+ After setup, test:
450
237
 
451
238
  1. correct email/password shows OTP screen
452
- 2. correct OTP logs into admin successfully
239
+ 2. correct OTP logs in successfully
453
240
  3. resend OTP works
454
241
  4. invalid OTP shows an error
455
242
  5. expired OTP restarts the flow properly
456
- 6. wrong email/password still fails safely
243
+ 6. wrong email/password fails safely
457
244
 
458
- ## Production Notes
245
+ ## Security Note
459
246
 
460
- - This improves admin security, but email OTP is still weaker than TOTP or WebAuthn.
461
- - If the admin email account is compromised, the second factor can still be bypassed.
462
- - For stronger future versions, consider TOTP, backup codes, trusted devices, or passkeys.
247
+ Email OTP improves admin security, but it is still weaker than TOTP, WebAuthn, or passkeys.
@@ -0,0 +1,115 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const rootDir = process.cwd();
5
+
6
+ const patchFiles = [
7
+ {
8
+ source: path.join(
9
+ rootDir,
10
+ "scripts",
11
+ "strapi-admin-2fa-patch",
12
+ "pages",
13
+ "Auth",
14
+ "components",
15
+ "Login.js"
16
+ ),
17
+ target: path.join(
18
+ rootDir,
19
+ "node_modules",
20
+ "@strapi",
21
+ "admin",
22
+ "dist",
23
+ "admin",
24
+ "admin",
25
+ "src",
26
+ "pages",
27
+ "Auth",
28
+ "components",
29
+ "Login.js"
30
+ ),
31
+ },
32
+ {
33
+ source: path.join(
34
+ rootDir,
35
+ "scripts",
36
+ "strapi-admin-2fa-patch",
37
+ "pages",
38
+ "Auth",
39
+ "components",
40
+ "Login.mjs"
41
+ ),
42
+ target: path.join(
43
+ rootDir,
44
+ "node_modules",
45
+ "@strapi",
46
+ "admin",
47
+ "dist",
48
+ "admin",
49
+ "admin",
50
+ "src",
51
+ "pages",
52
+ "Auth",
53
+ "components",
54
+ "Login.mjs"
55
+ ),
56
+ },
57
+ {
58
+ source: path.join(rootDir, "scripts", "strapi-admin-2fa-patch", "services", "auth.js"),
59
+ target: path.join(
60
+ rootDir,
61
+ "node_modules",
62
+ "@strapi",
63
+ "admin",
64
+ "dist",
65
+ "admin",
66
+ "admin",
67
+ "src",
68
+ "services",
69
+ "auth.js"
70
+ ),
71
+ },
72
+ {
73
+ source: path.join(rootDir, "scripts", "strapi-admin-2fa-patch", "services", "auth.mjs"),
74
+ target: path.join(
75
+ rootDir,
76
+ "node_modules",
77
+ "@strapi",
78
+ "admin",
79
+ "dist",
80
+ "admin",
81
+ "admin",
82
+ "src",
83
+ "services",
84
+ "auth.mjs"
85
+ ),
86
+ },
87
+ ];
88
+
89
+ const generatedCacheDirs = [
90
+ path.join(rootDir, "node_modules", ".strapi", "vite"),
91
+ path.join(rootDir, ".strapi", "client"),
92
+ ];
93
+
94
+ const missing = patchFiles.find(
95
+ ({ source, target }) => !fs.existsSync(source) || !fs.existsSync(target)
96
+ );
97
+
98
+ if (missing) {
99
+ console.warn("[admin-2fa-patch] Skipping Strapi admin patch because a source or target file is missing.");
100
+ process.exit(0);
101
+ }
102
+
103
+ for (const { source, target } of patchFiles) {
104
+ fs.copyFileSync(source, target);
105
+ }
106
+
107
+ for (const generatedDir of generatedCacheDirs) {
108
+ if (fs.existsSync(generatedDir)) {
109
+ fs.rmSync(generatedDir, { recursive: true, force: true });
110
+ }
111
+ }
112
+
113
+ console.log(
114
+ "[admin-2fa-patch] Applied Strapi admin OTP login patch and cleared Strapi admin caches."
115
+ );