@vivinkv28/strapi-2fa-admin-plugin 0.1.5 → 0.1.7
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 +314 -157
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
# @vivinkv28/strapi-2fa-admin-plugin
|
|
2
2
|
|
|
3
|
-
`@vivinkv28/strapi-2fa-admin-plugin` is a Strapi 5 plugin that
|
|
3
|
+
`@vivinkv28/strapi-2fa-admin-plugin` is a Strapi 5 plugin that adds the backend side of an OTP-based admin login flow.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It gives your Strapi project:
|
|
6
6
|
|
|
7
|
-
- admin credential validation
|
|
8
|
-
- OTP challenge
|
|
7
|
+
- admin credential validation before OTP
|
|
8
|
+
- OTP challenge creation
|
|
9
9
|
- OTP resend and verification
|
|
10
10
|
- rate limiting for login, verify, and resend
|
|
11
11
|
- OTP delivery through Strapi's email plugin
|
|
12
|
-
- final Strapi admin session creation after OTP verification
|
|
12
|
+
- final Strapi admin session creation only after OTP verification
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## What This Package Is
|
|
15
15
|
|
|
16
|
-
This package is a backend/admin-auth
|
|
16
|
+
This package is a **backend/admin-auth plugin**.
|
|
17
17
|
|
|
18
|
-
It does **not** replace the Strapi admin login UI
|
|
18
|
+
It does **not** automatically replace the default Strapi admin login UI.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
To use it in a real project, you need two parts:
|
|
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
|
|
25
30
|
|
|
26
31
|
## Endpoints
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
After installation, the plugin exposes:
|
|
29
34
|
|
|
30
35
|
- `POST /api/admin-2fa/login`
|
|
31
36
|
- `POST /api/admin-2fa/verify`
|
|
@@ -43,12 +48,11 @@ The plugin exposes these routes:
|
|
|
43
48
|
npm install @vivinkv28/strapi-2fa-admin-plugin
|
|
44
49
|
```
|
|
45
50
|
|
|
46
|
-
##
|
|
51
|
+
## Step 1: Enable The Plugin
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
In your Strapi project, update `config/plugins.ts`:
|
|
49
54
|
|
|
50
55
|
```ts
|
|
51
|
-
// config/plugins.ts
|
|
52
56
|
import type { Core } from "@strapi/strapi";
|
|
53
57
|
|
|
54
58
|
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Plugin => ({
|
|
@@ -77,42 +81,296 @@ const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Plugin =>
|
|
|
77
81
|
export default config;
|
|
78
82
|
```
|
|
79
83
|
|
|
80
|
-
##
|
|
84
|
+
## Step 2: Configure Email
|
|
85
|
+
|
|
86
|
+
This plugin sends OTP emails through Strapi's email plugin.
|
|
87
|
+
|
|
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.
|
|
91
|
+
|
|
92
|
+
## Step 3: Make Sure Server/Proxy Settings Are Correct
|
|
93
|
+
|
|
94
|
+
If your Strapi app runs behind a proxy, configure `config/server.ts` correctly so admin cookies work as expected.
|
|
95
|
+
|
|
96
|
+
Typical example:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import type { Core } from "@strapi/strapi";
|
|
100
|
+
|
|
101
|
+
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => ({
|
|
102
|
+
host: env("HOST", "0.0.0.0"),
|
|
103
|
+
port: env.int("PORT", 1337),
|
|
104
|
+
url: env("URL", "http://localhost:1337"),
|
|
105
|
+
proxy: env.bool("IS_PROXIED", env("NODE_ENV", "development") === "production"),
|
|
106
|
+
app: {
|
|
107
|
+
keys: env.array("APP_KEYS"),
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export default config;
|
|
112
|
+
```
|
|
113
|
+
|
|
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:
|
|
119
|
+
|
|
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:
|
|
130
|
+
|
|
131
|
+
```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
|
|
144
|
+
```
|
|
145
|
+
|
|
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:
|
|
151
|
+
|
|
152
|
+
- `scripts/strapi-admin-2fa-patch/services/auth.js`
|
|
153
|
+
- `scripts/strapi-admin-2fa-patch/services/auth.mjs`
|
|
154
|
+
|
|
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
|
|
81
196
|
|
|
82
|
-
|
|
197
|
+
```js
|
|
198
|
+
const {
|
|
199
|
+
useAdminLoginWithOtpMutation,
|
|
200
|
+
useVerifyAdminLoginOtpMutation,
|
|
201
|
+
useResendAdminLoginOtpMutation,
|
|
202
|
+
} = authService;
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Step 6: Patch The Strapi Login Screen
|
|
206
|
+
|
|
207
|
+
Create these files in your Strapi project:
|
|
83
208
|
|
|
84
|
-
|
|
209
|
+
- `scripts/strapi-admin-2fa-patch/pages/Auth/components/Login.js`
|
|
210
|
+
- `scripts/strapi-admin-2fa-patch/pages/Auth/components/Login.mjs`
|
|
85
211
|
|
|
86
|
-
|
|
212
|
+
This component must replace the normal one-step login with a two-step state:
|
|
87
213
|
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
- if successful, store `challengeId` and switch to OTP mode
|
|
214
|
+
- credentials step
|
|
215
|
+
- OTP step
|
|
91
216
|
|
|
92
|
-
|
|
217
|
+
### Minimum state you need
|
|
93
218
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
```
|
|
98
227
|
|
|
99
|
-
###
|
|
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
|
+
}
|
|
100
243
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
```
|
|
105
310
|
|
|
106
|
-
|
|
311
|
+
### OTP input handling
|
|
107
312
|
|
|
108
|
-
|
|
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
|
|
323
|
+
- 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`
|
|
333
|
+
|
|
334
|
+
This script should:
|
|
335
|
+
|
|
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
|
|
342
|
+
|
|
343
|
+
## Step 8: Wire The Patch Script In `package.json`
|
|
344
|
+
|
|
345
|
+
In your Strapi project `package.json`, add:
|
|
346
|
+
|
|
347
|
+
```json
|
|
348
|
+
{
|
|
349
|
+
"scripts": {
|
|
350
|
+
"prebuild": "node scripts/apply-strapi-admin-2fa-patch.js",
|
|
351
|
+
"predev": "node scripts/apply-strapi-admin-2fa-patch.js",
|
|
352
|
+
"predevelop": "node scripts/apply-strapi-admin-2fa-patch.js",
|
|
353
|
+
"postinstall": "node scripts/apply-strapi-admin-2fa-patch.js"
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
This ensures the login patch is reapplied:
|
|
359
|
+
|
|
360
|
+
- after dependency install
|
|
361
|
+
- before build
|
|
362
|
+
- before dev
|
|
363
|
+
|
|
364
|
+
## Request Flow
|
|
365
|
+
|
|
366
|
+
### Login
|
|
109
367
|
|
|
110
368
|
```http
|
|
111
369
|
POST /api/admin-2fa/login
|
|
112
370
|
Content-Type: application/json
|
|
113
371
|
```
|
|
114
372
|
|
|
115
|
-
Example
|
|
373
|
+
Example body:
|
|
116
374
|
|
|
117
375
|
```json
|
|
118
376
|
{
|
|
@@ -123,12 +381,12 @@ Example payload:
|
|
|
123
381
|
}
|
|
124
382
|
```
|
|
125
383
|
|
|
126
|
-
Example
|
|
384
|
+
Example response:
|
|
127
385
|
|
|
128
386
|
```json
|
|
129
387
|
{
|
|
130
388
|
"data": {
|
|
131
|
-
"challengeId": "
|
|
389
|
+
"challengeId": "***",
|
|
132
390
|
"expiresAt": "2026-04-05T18:30:00.000Z",
|
|
133
391
|
"maskedEmail": "admin@example.com",
|
|
134
392
|
"rememberMe": true
|
|
@@ -136,89 +394,37 @@ Example success response:
|
|
|
136
394
|
}
|
|
137
395
|
```
|
|
138
396
|
|
|
139
|
-
### Verify
|
|
397
|
+
### Verify
|
|
140
398
|
|
|
141
399
|
```http
|
|
142
400
|
POST /api/admin-2fa/verify
|
|
143
401
|
Content-Type: application/json
|
|
144
402
|
```
|
|
145
403
|
|
|
146
|
-
Example
|
|
404
|
+
Example body:
|
|
147
405
|
|
|
148
406
|
```json
|
|
149
407
|
{
|
|
150
|
-
"challengeId": "
|
|
408
|
+
"challengeId": "***",
|
|
151
409
|
"code": "123456"
|
|
152
410
|
}
|
|
153
411
|
```
|
|
154
412
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
```json
|
|
158
|
-
{
|
|
159
|
-
"data": {
|
|
160
|
-
"token": "<access-token>",
|
|
161
|
-
"accessToken": "<access-token>",
|
|
162
|
-
"user": {
|
|
163
|
-
"id": 1,
|
|
164
|
-
"email": "admin@example.com"
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### Resend request
|
|
413
|
+
### Resend
|
|
171
414
|
|
|
172
415
|
```http
|
|
173
416
|
POST /api/admin-2fa/resend
|
|
174
417
|
Content-Type: application/json
|
|
175
418
|
```
|
|
176
419
|
|
|
177
|
-
Example
|
|
420
|
+
Example body:
|
|
178
421
|
|
|
179
422
|
```json
|
|
180
423
|
{
|
|
181
|
-
"challengeId": "
|
|
424
|
+
"challengeId": "***"
|
|
182
425
|
}
|
|
183
426
|
```
|
|
184
427
|
|
|
185
|
-
### UI error states to handle
|
|
186
|
-
|
|
187
|
-
- invalid email or password
|
|
188
|
-
- OTP expired
|
|
189
|
-
- OTP session not found
|
|
190
|
-
- invalid OTP code
|
|
191
|
-
- too many authentication attempts
|
|
192
|
-
- maximum resend attempts exceeded
|
|
193
|
-
|
|
194
|
-
## Host Project Requirements
|
|
195
|
-
|
|
196
|
-
### Email provider
|
|
197
|
-
|
|
198
|
-
The plugin sends OTP emails through Strapi's email plugin, so the host project must configure an email provider.
|
|
199
|
-
|
|
200
|
-
### Proxy and HTTPS
|
|
201
|
-
|
|
202
|
-
If the project runs behind a reverse proxy, configure `config/server.ts` correctly so secure admin cookies work.
|
|
203
|
-
|
|
204
|
-
Typical example:
|
|
205
|
-
|
|
206
|
-
```ts
|
|
207
|
-
import type { Core } from "@strapi/strapi";
|
|
208
|
-
|
|
209
|
-
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => ({
|
|
210
|
-
host: env("HOST", "0.0.0.0"),
|
|
211
|
-
port: env.int("PORT", 1337),
|
|
212
|
-
url: env("URL", "http://localhost:1337"),
|
|
213
|
-
proxy: env.bool("IS_PROXIED", env("NODE_ENV", "development") === "production"),
|
|
214
|
-
app: {
|
|
215
|
-
keys: env.array("APP_KEYS"),
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
export default config;
|
|
220
|
-
```
|
|
221
|
-
|
|
222
428
|
## Environment Variables
|
|
223
429
|
|
|
224
430
|
Suggested defaults:
|
|
@@ -238,68 +444,19 @@ ADMIN_OTP_RESEND_EMAIL_LIMIT=5
|
|
|
238
444
|
ADMIN_OTP_DEBUG_TIMINGS=false
|
|
239
445
|
```
|
|
240
446
|
|
|
241
|
-
##
|
|
242
|
-
|
|
243
|
-
Main files:
|
|
244
|
-
|
|
245
|
-
```text
|
|
246
|
-
admin/src/index.js
|
|
247
|
-
server/src/index.js
|
|
248
|
-
server/src/routes/index.js
|
|
249
|
-
server/src/controllers/auth.js
|
|
250
|
-
server/src/services/auth.js
|
|
251
|
-
server/src/utils/strapi-session-auth.js
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
Responsibilities:
|
|
255
|
-
|
|
256
|
-
- `admin/src/index.js`
|
|
257
|
-
Minimal admin plugin stub required by the Strapi Plugin SDK.
|
|
258
|
-
|
|
259
|
-
- `server/src/routes/index.js`
|
|
260
|
-
Declares the login, verify, and resend routes.
|
|
261
|
-
|
|
262
|
-
- `server/src/controllers/auth.js`
|
|
263
|
-
Reads the request, extracts client IP, delegates to the service, and sets the admin refresh cookie after successful OTP verification.
|
|
264
|
-
|
|
265
|
-
- `server/src/services/auth.js`
|
|
266
|
-
Core OTP logic: credential validation, challenge lifecycle, resend/verify rules, rate limiting, email sending, and final session creation.
|
|
267
|
-
|
|
268
|
-
- `server/src/utils/strapi-session-auth.js`
|
|
269
|
-
Runtime helper that resolves Strapi's internal admin session utility for final session creation.
|
|
270
|
-
|
|
271
|
-
## Repo Docs
|
|
272
|
-
|
|
273
|
-
If you are reading the source repository directly, deeper docs are also available in:
|
|
274
|
-
|
|
275
|
-
- `docs/INTEGRATION.md`
|
|
276
|
-
- `docs/ARCHITECTURE.md`
|
|
277
|
-
|
|
278
|
-
## Development
|
|
279
|
-
|
|
280
|
-
```bash
|
|
281
|
-
npm install
|
|
282
|
-
npm run build
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
Useful commands:
|
|
286
|
-
|
|
287
|
-
- `npm run build`
|
|
288
|
-
- `npm run watch`
|
|
289
|
-
- `npm run watch:link`
|
|
290
|
-
- `npm run verify`
|
|
447
|
+
## Testing Checklist
|
|
291
448
|
|
|
292
|
-
|
|
449
|
+
After setup, test these cases:
|
|
293
450
|
|
|
294
|
-
1.
|
|
295
|
-
2.
|
|
296
|
-
3.
|
|
297
|
-
4.
|
|
298
|
-
5.
|
|
299
|
-
6.
|
|
451
|
+
1. correct email/password shows OTP screen
|
|
452
|
+
2. correct OTP logs into admin successfully
|
|
453
|
+
3. resend OTP works
|
|
454
|
+
4. invalid OTP shows an error
|
|
455
|
+
5. expired OTP restarts the flow properly
|
|
456
|
+
6. wrong email/password still fails safely
|
|
300
457
|
|
|
301
458
|
## Production Notes
|
|
302
459
|
|
|
303
|
-
-
|
|
304
|
-
- If the admin
|
|
305
|
-
- For stronger
|
|
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.
|