@vivinkv28/strapi-2fa-admin-plugin 0.1.0
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/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/admin/index.js +15 -0
- package/dist/admin/index.mjs +16 -0
- package/dist/server/index.js +44178 -0
- package/dist/server/index.mjs +44166 -0
- package/docs/ARCHITECTURE.md +203 -0
- package/docs/INTEGRATION.md +246 -0
- package/package.json +61 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Architecture Guide
|
|
2
|
+
|
|
3
|
+
This guide explains the internal structure of `@vivinkv28/strapi-2fa-admin-plugin` for maintainers and contributors.
|
|
4
|
+
|
|
5
|
+
## High-Level Design
|
|
6
|
+
|
|
7
|
+
The plugin is a backend-focused Strapi 5 plugin with a minimal admin stub.
|
|
8
|
+
|
|
9
|
+
Its responsibilities are:
|
|
10
|
+
|
|
11
|
+
- validate Strapi admin credentials
|
|
12
|
+
- create a temporary OTP challenge
|
|
13
|
+
- send an OTP by email
|
|
14
|
+
- verify the OTP
|
|
15
|
+
- create the final Strapi admin session only after OTP verification
|
|
16
|
+
|
|
17
|
+
Its non-responsibilities are:
|
|
18
|
+
|
|
19
|
+
- replacing the Strapi admin login UI
|
|
20
|
+
- rendering a custom OTP page inside the admin by itself
|
|
21
|
+
|
|
22
|
+
## Folder Structure
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
admin/src/index.js
|
|
26
|
+
server/src/index.js
|
|
27
|
+
server/src/routes/index.js
|
|
28
|
+
server/src/controllers/auth.js
|
|
29
|
+
server/src/services/auth.js
|
|
30
|
+
server/src/utils/strapi-session-auth.js
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Entry Points
|
|
34
|
+
|
|
35
|
+
### `admin/src/index.js`
|
|
36
|
+
|
|
37
|
+
This is a minimal admin-side plugin entry required by the Strapi Plugin SDK.
|
|
38
|
+
|
|
39
|
+
Right now it only provides:
|
|
40
|
+
|
|
41
|
+
- `register()`
|
|
42
|
+
- `bootstrap()`
|
|
43
|
+
- `registerTrads()`
|
|
44
|
+
|
|
45
|
+
It does not add real admin UI behavior.
|
|
46
|
+
|
|
47
|
+
### `server/src/index.js`
|
|
48
|
+
|
|
49
|
+
This is the server-side plugin entry. It wires together:
|
|
50
|
+
|
|
51
|
+
- controllers
|
|
52
|
+
- routes
|
|
53
|
+
- services
|
|
54
|
+
|
|
55
|
+
## Routes
|
|
56
|
+
|
|
57
|
+
Defined in [`server/src/routes/index.js`](../server/src/routes/index.js).
|
|
58
|
+
|
|
59
|
+
The plugin registers a `content-api` router with these logical paths:
|
|
60
|
+
|
|
61
|
+
- `/login`
|
|
62
|
+
- `/verify`
|
|
63
|
+
- `/resend`
|
|
64
|
+
|
|
65
|
+
Because Strapi prefixes plugin content-api routes with the plugin name, these become:
|
|
66
|
+
|
|
67
|
+
- `/api/admin-2fa/login`
|
|
68
|
+
- `/api/admin-2fa/verify`
|
|
69
|
+
- `/api/admin-2fa/resend`
|
|
70
|
+
|
|
71
|
+
## Controller Layer
|
|
72
|
+
|
|
73
|
+
Defined in [`server/src/controllers/auth.js`](../server/src/controllers/auth.js).
|
|
74
|
+
|
|
75
|
+
The controller is intentionally thin:
|
|
76
|
+
|
|
77
|
+
- reads request body
|
|
78
|
+
- reads client IP from proxy-aware request headers
|
|
79
|
+
- calls the auth service
|
|
80
|
+
- sets the Strapi admin refresh cookie after successful OTP verification
|
|
81
|
+
- returns response data in Strapi-style `{ data: ... }` format
|
|
82
|
+
|
|
83
|
+
### Client IP handling
|
|
84
|
+
|
|
85
|
+
The controller checks:
|
|
86
|
+
|
|
87
|
+
1. `x-forwarded-for`
|
|
88
|
+
2. `ctx.request.ip`
|
|
89
|
+
3. `ctx.ip`
|
|
90
|
+
|
|
91
|
+
This is important because the service rate-limits by IP.
|
|
92
|
+
|
|
93
|
+
## Service Layer
|
|
94
|
+
|
|
95
|
+
Defined in [`server/src/services/auth.js`](../server/src/services/auth.js).
|
|
96
|
+
|
|
97
|
+
This is the core of the plugin.
|
|
98
|
+
|
|
99
|
+
### Main responsibilities
|
|
100
|
+
|
|
101
|
+
- read plugin config from `strapi.config.get("plugin::admin-2fa")`
|
|
102
|
+
- normalize and validate request input
|
|
103
|
+
- call `strapi.service("admin::auth").checkCredentials(...)`
|
|
104
|
+
- generate OTP codes
|
|
105
|
+
- hash OTP values with `crypto.scrypt`
|
|
106
|
+
- store challenge state in `strapi.store`
|
|
107
|
+
- enforce resend/verify/login rate limits
|
|
108
|
+
- send OTP email using Strapi email plugin
|
|
109
|
+
- create the final Strapi admin session after OTP verification
|
|
110
|
+
|
|
111
|
+
### Important internal helpers
|
|
112
|
+
|
|
113
|
+
#### `getPluginConfig()`
|
|
114
|
+
|
|
115
|
+
Builds the runtime configuration using plugin config values with defaults.
|
|
116
|
+
|
|
117
|
+
#### `createOtpCode()`
|
|
118
|
+
|
|
119
|
+
Generates a numeric OTP code with configurable length.
|
|
120
|
+
|
|
121
|
+
#### `createOtpHash()`
|
|
122
|
+
|
|
123
|
+
Hashes the OTP using:
|
|
124
|
+
|
|
125
|
+
- `challengeId`
|
|
126
|
+
- `code`
|
|
127
|
+
- random `salt`
|
|
128
|
+
|
|
129
|
+
The raw OTP is never stored.
|
|
130
|
+
|
|
131
|
+
#### `registerRateLimitHit()`
|
|
132
|
+
|
|
133
|
+
Stores rate-limit counters in `strapi.store`.
|
|
134
|
+
|
|
135
|
+
Rate-limit keys are hashed with SHA-256 so raw identifiers are not used directly as store keys.
|
|
136
|
+
|
|
137
|
+
#### `createSession()`
|
|
138
|
+
|
|
139
|
+
Uses Strapi's internal admin session helpers to generate:
|
|
140
|
+
|
|
141
|
+
- refresh token
|
|
142
|
+
- cookie options
|
|
143
|
+
- access token
|
|
144
|
+
- sanitized admin user
|
|
145
|
+
|
|
146
|
+
## Challenge Storage
|
|
147
|
+
|
|
148
|
+
The plugin uses `strapi.store()` with:
|
|
149
|
+
|
|
150
|
+
- type: `plugin`
|
|
151
|
+
- name: `admin-otp-login`
|
|
152
|
+
|
|
153
|
+
This is used for:
|
|
154
|
+
|
|
155
|
+
- OTP challenges
|
|
156
|
+
- rate-limit entries
|
|
157
|
+
|
|
158
|
+
This keeps OTP state internal to Strapi rather than creating a public collection type.
|
|
159
|
+
|
|
160
|
+
## Session Helper
|
|
161
|
+
|
|
162
|
+
Defined in [`server/src/utils/strapi-session-auth.js`](../server/src/utils/strapi-session-auth.js).
|
|
163
|
+
|
|
164
|
+
Strapi does not expose the needed admin session helper directly as a simple public import for this use case, so the plugin resolves the installed Strapi admin `session-auth.js` helper at runtime.
|
|
165
|
+
|
|
166
|
+
This is one of the more sensitive parts of the implementation because it depends on Strapi internal package layout.
|
|
167
|
+
|
|
168
|
+
## Security Model
|
|
169
|
+
|
|
170
|
+
### Good properties
|
|
171
|
+
|
|
172
|
+
- password alone does not create a final admin session
|
|
173
|
+
- OTP is hashed, not stored in plain text
|
|
174
|
+
- OTPs expire
|
|
175
|
+
- resend attempts are limited
|
|
176
|
+
- verify attempts are limited
|
|
177
|
+
- login/verify/resend rate limits exist
|
|
178
|
+
|
|
179
|
+
### Limitations
|
|
180
|
+
|
|
181
|
+
- this is email OTP, not TOTP or WebAuthn
|
|
182
|
+
- if the admin email account is compromised, the second factor can still be bypassed
|
|
183
|
+
- session creation still depends on Strapi internal admin session utilities
|
|
184
|
+
|
|
185
|
+
## Extension Points
|
|
186
|
+
|
|
187
|
+
If you want to extend this plugin later, common directions are:
|
|
188
|
+
|
|
189
|
+
- customizable email templates
|
|
190
|
+
- audit logging
|
|
191
|
+
- backup codes
|
|
192
|
+
- trusted devices
|
|
193
|
+
- TOTP support
|
|
194
|
+
- WebAuthn/passkeys
|
|
195
|
+
|
|
196
|
+
## Integration Boundary
|
|
197
|
+
|
|
198
|
+
The clean boundary is:
|
|
199
|
+
|
|
200
|
+
- plugin: backend 2FA engine
|
|
201
|
+
- host app: admin login UI interception and OTP screen behavior
|
|
202
|
+
|
|
203
|
+
That split is intentional and should stay explicit in docs and versioning.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Integration Guide
|
|
2
|
+
|
|
3
|
+
This guide explains how to use `@vivinkv28/strapi-2fa-admin-plugin` inside an existing Strapi 5 project.
|
|
4
|
+
|
|
5
|
+
## What The Plugin Does
|
|
6
|
+
|
|
7
|
+
The plugin provides the backend endpoints and auth logic for an OTP-based admin login flow.
|
|
8
|
+
|
|
9
|
+
It does not replace the Strapi admin login UI by itself.
|
|
10
|
+
|
|
11
|
+
Your host project must provide a login integration layer that:
|
|
12
|
+
|
|
13
|
+
1. collects admin email and password
|
|
14
|
+
2. calls the plugin login endpoint
|
|
15
|
+
3. shows an OTP input screen
|
|
16
|
+
4. calls the plugin verify endpoint
|
|
17
|
+
5. optionally allows OTP resend
|
|
18
|
+
|
|
19
|
+
## Endpoints
|
|
20
|
+
|
|
21
|
+
The plugin registers these routes:
|
|
22
|
+
|
|
23
|
+
- `POST /api/admin-2fa/login`
|
|
24
|
+
- `POST /api/admin-2fa/verify`
|
|
25
|
+
- `POST /api/admin-2fa/resend`
|
|
26
|
+
|
|
27
|
+
## Install And Enable
|
|
28
|
+
|
|
29
|
+
### Published npm package
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @vivinkv28/strapi-2fa-admin-plugin
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Local external plugin
|
|
36
|
+
|
|
37
|
+
In your Strapi app:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// config/plugins.ts
|
|
41
|
+
import type { Core } from "@strapi/strapi";
|
|
42
|
+
|
|
43
|
+
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Plugin => ({
|
|
44
|
+
"admin-2fa": {
|
|
45
|
+
enabled: true,
|
|
46
|
+
resolve: "../strapi-2fa-admin-plugin",
|
|
47
|
+
config: {
|
|
48
|
+
otpDigits: env.int("ADMIN_OTP_DIGITS", 6),
|
|
49
|
+
otpTtlSeconds: env.int("ADMIN_OTP_TTL_SECONDS", 300),
|
|
50
|
+
maxAttempts: env.int("ADMIN_OTP_MAX_ATTEMPTS", 5),
|
|
51
|
+
maxResends: env.int("ADMIN_OTP_MAX_RESENDS", 3),
|
|
52
|
+
rateLimitWindowSeconds: env.int("ADMIN_OTP_RATE_LIMIT_WINDOW_SECONDS", 900),
|
|
53
|
+
loginIpLimit: env.int("ADMIN_OTP_LOGIN_IP_LIMIT", 10),
|
|
54
|
+
loginEmailLimit: env.int("ADMIN_OTP_LOGIN_EMAIL_LIMIT", 5),
|
|
55
|
+
verifyIpLimit: env.int("ADMIN_OTP_VERIFY_IP_LIMIT", 20),
|
|
56
|
+
verifyEmailLimit: env.int("ADMIN_OTP_VERIFY_EMAIL_LIMIT", 10),
|
|
57
|
+
resendIpLimit: env.int("ADMIN_OTP_RESEND_IP_LIMIT", 10),
|
|
58
|
+
resendEmailLimit: env.int("ADMIN_OTP_RESEND_EMAIL_LIMIT", 5),
|
|
59
|
+
debugTimings: env.bool(
|
|
60
|
+
"ADMIN_OTP_DEBUG_TIMINGS",
|
|
61
|
+
env("NODE_ENV", "development") !== "production"
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export default config;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Required Host App Setup
|
|
71
|
+
|
|
72
|
+
### Email provider
|
|
73
|
+
|
|
74
|
+
The plugin calls `strapi.plugin("email").service("email").send(...)`.
|
|
75
|
+
|
|
76
|
+
Your Strapi project must configure an email provider or OTP delivery will fail.
|
|
77
|
+
|
|
78
|
+
### Proxy and HTTPS
|
|
79
|
+
|
|
80
|
+
If your project runs behind a reverse proxy, configure `config/server.ts` correctly so Strapi recognizes secure requests and admin cookies are set with the right options.
|
|
81
|
+
|
|
82
|
+
Typical example:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import type { Core } from "@strapi/strapi";
|
|
86
|
+
|
|
87
|
+
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => ({
|
|
88
|
+
host: env("HOST", "0.0.0.0"),
|
|
89
|
+
port: env.int("PORT", 1337),
|
|
90
|
+
url: env("URL", "http://localhost:1337"),
|
|
91
|
+
proxy: env.bool("IS_PROXIED", env("NODE_ENV", "development") === "production"),
|
|
92
|
+
app: {
|
|
93
|
+
keys: env.array("APP_KEYS"),
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export default config;
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Request Flow
|
|
101
|
+
|
|
102
|
+
### 1. Start login
|
|
103
|
+
|
|
104
|
+
Send admin email and password to:
|
|
105
|
+
|
|
106
|
+
```http
|
|
107
|
+
POST /api/admin-2fa/login
|
|
108
|
+
Content-Type: application/json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Example payload:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"email": "admin@example.com",
|
|
116
|
+
"password": "super-secret-password",
|
|
117
|
+
"rememberMe": true,
|
|
118
|
+
"deviceId": "browser-device-id"
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Example success response:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"data": {
|
|
127
|
+
"challengeId": "0d3af6fd-b351-4d1e-bb81-2a8201d8a0f4",
|
|
128
|
+
"expiresAt": "2026-04-05T18:30:00.000Z",
|
|
129
|
+
"maskedEmail": "admin@example.com",
|
|
130
|
+
"rememberMe": true
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
What happens here:
|
|
136
|
+
|
|
137
|
+
- Strapi validates the admin credentials
|
|
138
|
+
- the plugin creates an OTP challenge
|
|
139
|
+
- the plugin emails the OTP
|
|
140
|
+
- no final admin session is created yet
|
|
141
|
+
|
|
142
|
+
### 2. Verify OTP
|
|
143
|
+
|
|
144
|
+
Send the OTP code to:
|
|
145
|
+
|
|
146
|
+
```http
|
|
147
|
+
POST /api/admin-2fa/verify
|
|
148
|
+
Content-Type: application/json
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Example payload:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"challengeId": "0d3af6fd-b351-4d1e-bb81-2a8201d8a0f4",
|
|
156
|
+
"code": "123456"
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Example success response:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"data": {
|
|
165
|
+
"token": "<access-token>",
|
|
166
|
+
"accessToken": "<access-token>",
|
|
167
|
+
"user": {
|
|
168
|
+
"id": 1,
|
|
169
|
+
"email": "admin@example.com"
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
What happens here:
|
|
176
|
+
|
|
177
|
+
- the plugin reloads the OTP challenge
|
|
178
|
+
- the submitted code is hashed and compared
|
|
179
|
+
- the real Strapi admin session is created only after OTP verification
|
|
180
|
+
- the refresh cookie is set by the plugin controller
|
|
181
|
+
|
|
182
|
+
### 3. Resend OTP
|
|
183
|
+
|
|
184
|
+
Send:
|
|
185
|
+
|
|
186
|
+
```http
|
|
187
|
+
POST /api/admin-2fa/resend
|
|
188
|
+
Content-Type: application/json
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Example payload:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"challengeId": "0d3af6fd-b351-4d1e-bb81-2a8201d8a0f4"
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
This generates a new OTP for the same challenge and extends the expiry.
|
|
200
|
+
|
|
201
|
+
## Error Cases To Handle In The UI
|
|
202
|
+
|
|
203
|
+
Your login integration should handle these cases cleanly:
|
|
204
|
+
|
|
205
|
+
- invalid email or password
|
|
206
|
+
- OTP session not found
|
|
207
|
+
- OTP expired
|
|
208
|
+
- invalid OTP code
|
|
209
|
+
- too many authentication attempts
|
|
210
|
+
- maximum resend attempts exceeded
|
|
211
|
+
|
|
212
|
+
These are returned as normal Strapi error responses, so the frontend should surface the message to the admin user in a safe and friendly way.
|
|
213
|
+
|
|
214
|
+
## Recommended Frontend Behavior
|
|
215
|
+
|
|
216
|
+
- Keep login and OTP entry as two separate UI states.
|
|
217
|
+
- Store `challengeId` in memory for the current login attempt.
|
|
218
|
+
- Do not create your own session after password validation; wait for `/verify`.
|
|
219
|
+
- After successful OTP verification, treat the returned user/token exactly like a successful admin login.
|
|
220
|
+
- If resend fails due to limits or expiry, restart the login flow.
|
|
221
|
+
|
|
222
|
+
## Environment Variables
|
|
223
|
+
|
|
224
|
+
Recommended defaults:
|
|
225
|
+
|
|
226
|
+
```env
|
|
227
|
+
ADMIN_OTP_DIGITS=6
|
|
228
|
+
ADMIN_OTP_TTL_SECONDS=300
|
|
229
|
+
ADMIN_OTP_MAX_ATTEMPTS=5
|
|
230
|
+
ADMIN_OTP_MAX_RESENDS=3
|
|
231
|
+
ADMIN_OTP_RATE_LIMIT_WINDOW_SECONDS=900
|
|
232
|
+
ADMIN_OTP_LOGIN_IP_LIMIT=10
|
|
233
|
+
ADMIN_OTP_LOGIN_EMAIL_LIMIT=5
|
|
234
|
+
ADMIN_OTP_VERIFY_IP_LIMIT=20
|
|
235
|
+
ADMIN_OTP_VERIFY_EMAIL_LIMIT=10
|
|
236
|
+
ADMIN_OTP_RESEND_IP_LIMIT=10
|
|
237
|
+
ADMIN_OTP_RESEND_EMAIL_LIMIT=5
|
|
238
|
+
ADMIN_OTP_DEBUG_TIMINGS=false
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Production Notes
|
|
242
|
+
|
|
243
|
+
- Email OTP is better than password-only login, but weaker than TOTP or WebAuthn.
|
|
244
|
+
- If the admin mailbox is compromised, the second factor can still be bypassed.
|
|
245
|
+
- Use app passwords or provider-managed SMTP credentials for OTP delivery.
|
|
246
|
+
- Make sure your host project sets `URL` and proxy settings correctly in production.
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vivinkv28/strapi-2fa-admin-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reusable Strapi admin 2FA plugin",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"strapi",
|
|
8
|
+
"strapi-plugin",
|
|
9
|
+
"strapi-v5",
|
|
10
|
+
"2fa",
|
|
11
|
+
"otp",
|
|
12
|
+
"admin-auth"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"docs"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
"./package.json": "./package.json",
|
|
20
|
+
"./strapi-admin": {
|
|
21
|
+
"source": "./admin/src/index.js",
|
|
22
|
+
"import": "./dist/admin/index.mjs",
|
|
23
|
+
"require": "./dist/admin/index.js",
|
|
24
|
+
"default": "./dist/admin/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./strapi-server": {
|
|
27
|
+
"source": "./server/src/index.js",
|
|
28
|
+
"import": "./dist/server/index.mjs",
|
|
29
|
+
"require": "./dist/server/index.js",
|
|
30
|
+
"default": "./dist/server/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "strapi-plugin build",
|
|
35
|
+
"watch": "strapi-plugin watch",
|
|
36
|
+
"watch:link": "strapi-plugin watch:link",
|
|
37
|
+
"verify": "strapi-plugin verify"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20.0.0 <=22.x.x",
|
|
41
|
+
"npm": ">=9.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@strapi/sdk-plugin": "^5.0.0",
|
|
45
|
+
"prettier": "^3.0.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@strapi/strapi": "^5.0.0",
|
|
49
|
+
"react": "^18.0.0",
|
|
50
|
+
"react-dom": "^18.0.0",
|
|
51
|
+
"react-router-dom": "^6.0.0",
|
|
52
|
+
"styled-components": "^6.0.0"
|
|
53
|
+
},
|
|
54
|
+
"strapi": {
|
|
55
|
+
"name": "admin-2fa",
|
|
56
|
+
"displayName": "Admin 2FA",
|
|
57
|
+
"description": "Adds OTP-based 2FA for Strapi admin authentication",
|
|
58
|
+
"kind": "plugin"
|
|
59
|
+
},
|
|
60
|
+
"license": "MIT"
|
|
61
|
+
}
|