@autional/plugin-mfa 0.2.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/README.md +43 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +417 -0
- package/dist/index.mjs +387 -0
- package/package.json +57 -0
- package/src/BackupCodes.tsx +60 -0
- package/src/MfaGuard.tsx +111 -0
- package/src/MfaSetup.tsx +170 -0
- package/src/__tests__/index.test.tsx +27 -0
- package/src/index.ts +4 -0
- package/src/mfa.ts +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @authms/plugin-mfa
|
|
2
|
+
|
|
3
|
+
AuthMS MFA Plugin — Multi-Factor Authentication UI components and flows for `@authms/react`.
|
|
4
|
+
|
|
5
|
+
## Components
|
|
6
|
+
|
|
7
|
+
- **`MfaSetup`** — guided TOTP setup with QR code display and verification
|
|
8
|
+
- **`MfaGuard`** — wrapper that requires MFA challenge before rendering children
|
|
9
|
+
- **`BackupCodes`** — display and regenerate one-time backup codes
|
|
10
|
+
- **`mfaPlugin`** — plugin factory to register with `AuthMS.use()`
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @authms/core @authms/react @authms/plugin-mfa
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { AuthmsProvider, useAuthms } from '@authms/react';
|
|
22
|
+
import { MfaSetup, MfaGuard, BackupCodes } from '@authms/plugin-mfa';
|
|
23
|
+
|
|
24
|
+
function SecurityPage() {
|
|
25
|
+
return (
|
|
26
|
+
<div>
|
|
27
|
+
<h2>Two-Factor Authentication</h2>
|
|
28
|
+
<MfaSetup />
|
|
29
|
+
<BackupCodes />
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function Dashboard() {
|
|
35
|
+
return (
|
|
36
|
+
<MfaGuard>
|
|
37
|
+
<h1>Sensitive Content</h1>
|
|
38
|
+
</MfaGuard>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
See the [root SDK README](../../README.md) for full documentation.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { AuthmsPlugin } from '@authms/core';
|
|
4
|
+
|
|
5
|
+
declare function MfaSetup(): react.JSX.Element;
|
|
6
|
+
|
|
7
|
+
declare function MfaGuard({ children }: {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}): react.JSX.Element;
|
|
10
|
+
|
|
11
|
+
declare function BackupCodes({ codes }: {
|
|
12
|
+
codes: string[];
|
|
13
|
+
}): react.JSX.Element;
|
|
14
|
+
|
|
15
|
+
declare function mfaPlugin(): AuthmsPlugin;
|
|
16
|
+
|
|
17
|
+
export { BackupCodes, MfaGuard, MfaSetup, mfaPlugin };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { AuthmsPlugin } from '@authms/core';
|
|
4
|
+
|
|
5
|
+
declare function MfaSetup(): react.JSX.Element;
|
|
6
|
+
|
|
7
|
+
declare function MfaGuard({ children }: {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}): react.JSX.Element;
|
|
10
|
+
|
|
11
|
+
declare function BackupCodes({ codes }: {
|
|
12
|
+
codes: string[];
|
|
13
|
+
}): react.JSX.Element;
|
|
14
|
+
|
|
15
|
+
declare function mfaPlugin(): AuthmsPlugin;
|
|
16
|
+
|
|
17
|
+
export { BackupCodes, MfaGuard, MfaSetup, mfaPlugin };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
BackupCodes: () => BackupCodes,
|
|
24
|
+
MfaGuard: () => MfaGuard,
|
|
25
|
+
MfaSetup: () => MfaSetup,
|
|
26
|
+
mfaPlugin: () => mfaPlugin
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/MfaSetup.tsx
|
|
31
|
+
var import_react2 = require("react");
|
|
32
|
+
var import_react3 = require("@authms/react");
|
|
33
|
+
|
|
34
|
+
// src/BackupCodes.tsx
|
|
35
|
+
var import_react = require("react");
|
|
36
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
37
|
+
var styles = {
|
|
38
|
+
wrapper: { marginTop: 20 },
|
|
39
|
+
warning: {
|
|
40
|
+
background: "#fef3c7",
|
|
41
|
+
border: "1px solid #f59e0b",
|
|
42
|
+
borderRadius: 6,
|
|
43
|
+
padding: "10px 14px",
|
|
44
|
+
marginBottom: 14,
|
|
45
|
+
fontSize: 13,
|
|
46
|
+
color: "#92400e"
|
|
47
|
+
},
|
|
48
|
+
codeList: {
|
|
49
|
+
background: "#1f2937",
|
|
50
|
+
color: "#e5e7eb",
|
|
51
|
+
borderRadius: 8,
|
|
52
|
+
padding: "14px 18px",
|
|
53
|
+
fontFamily: "monospace",
|
|
54
|
+
fontSize: 14,
|
|
55
|
+
lineHeight: 2,
|
|
56
|
+
display: "grid",
|
|
57
|
+
gridTemplateColumns: "1fr 1fr",
|
|
58
|
+
gap: "0 16px",
|
|
59
|
+
marginBottom: 14
|
|
60
|
+
},
|
|
61
|
+
button: {
|
|
62
|
+
width: "100%",
|
|
63
|
+
padding: "8px 0",
|
|
64
|
+
background: "#fff",
|
|
65
|
+
color: "#374151",
|
|
66
|
+
border: "1px solid #d1d5db",
|
|
67
|
+
borderRadius: 6,
|
|
68
|
+
fontSize: 13,
|
|
69
|
+
fontWeight: 500,
|
|
70
|
+
cursor: "pointer"
|
|
71
|
+
},
|
|
72
|
+
copied: {
|
|
73
|
+
width: "100%",
|
|
74
|
+
padding: "8px 0",
|
|
75
|
+
background: "#ecfdf5",
|
|
76
|
+
color: "#065f46",
|
|
77
|
+
border: "1px solid #6ee7b7",
|
|
78
|
+
borderRadius: 6,
|
|
79
|
+
fontSize: 13,
|
|
80
|
+
fontWeight: 500
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
function BackupCodes({ codes }) {
|
|
84
|
+
const [copied, setCopied] = (0, import_react.useState)(false);
|
|
85
|
+
async function handleCopy() {
|
|
86
|
+
await navigator.clipboard.writeText(codes.join("\n"));
|
|
87
|
+
setCopied(true);
|
|
88
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
89
|
+
}
|
|
90
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.wrapper, children: [
|
|
91
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.warning, children: "\u26A0 Store these recovery codes in a safe place. Each code can only be used once. If you lose access to your authenticator app, these codes are the only way to recover your account." }),
|
|
92
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.codeList, children: codes.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: c }, i)) }),
|
|
93
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
94
|
+
"button",
|
|
95
|
+
{
|
|
96
|
+
style: copied ? styles.copied : styles.button,
|
|
97
|
+
onClick: handleCopy,
|
|
98
|
+
children: copied ? "Copied!" : "Copy All Codes"
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
] });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/MfaSetup.tsx
|
|
105
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
106
|
+
var styles2 = {
|
|
107
|
+
container: { maxWidth: 420, margin: "0 auto", padding: 24 },
|
|
108
|
+
title: { fontSize: 20, fontWeight: 600, marginBottom: 8 },
|
|
109
|
+
subtitle: { fontSize: 14, color: "#6b7280", marginBottom: 20 },
|
|
110
|
+
qrPlaceholder: {
|
|
111
|
+
width: 200,
|
|
112
|
+
height: 200,
|
|
113
|
+
margin: "0 auto 16px",
|
|
114
|
+
background: "#f3f4f6",
|
|
115
|
+
borderRadius: 8,
|
|
116
|
+
display: "flex",
|
|
117
|
+
alignItems: "center",
|
|
118
|
+
justifyContent: "center",
|
|
119
|
+
fontSize: 13,
|
|
120
|
+
color: "#9ca3af",
|
|
121
|
+
textAlign: "center"
|
|
122
|
+
},
|
|
123
|
+
secretBox: {
|
|
124
|
+
background: "#f9fafb",
|
|
125
|
+
border: "1px solid #e5e7eb",
|
|
126
|
+
borderRadius: 6,
|
|
127
|
+
padding: "10px 14px",
|
|
128
|
+
marginBottom: 20,
|
|
129
|
+
fontFamily: "monospace",
|
|
130
|
+
fontSize: 14,
|
|
131
|
+
wordBreak: "break-all"
|
|
132
|
+
},
|
|
133
|
+
input: {
|
|
134
|
+
width: "100%",
|
|
135
|
+
padding: "10px 12px",
|
|
136
|
+
fontSize: 16,
|
|
137
|
+
border: "1px solid #d1d5db",
|
|
138
|
+
borderRadius: 6,
|
|
139
|
+
outline: "none",
|
|
140
|
+
boxSizing: "border-box"
|
|
141
|
+
},
|
|
142
|
+
button: {
|
|
143
|
+
width: "100%",
|
|
144
|
+
padding: "10px 0",
|
|
145
|
+
marginTop: 12,
|
|
146
|
+
background: "#4f46e5",
|
|
147
|
+
color: "#fff",
|
|
148
|
+
border: "none",
|
|
149
|
+
borderRadius: 6,
|
|
150
|
+
fontSize: 14,
|
|
151
|
+
fontWeight: 500,
|
|
152
|
+
cursor: "pointer"
|
|
153
|
+
},
|
|
154
|
+
buttonDisabled: {
|
|
155
|
+
width: "100%",
|
|
156
|
+
padding: "10px 0",
|
|
157
|
+
marginTop: 12,
|
|
158
|
+
background: "#9ca3af",
|
|
159
|
+
color: "#fff",
|
|
160
|
+
border: "none",
|
|
161
|
+
borderRadius: 6,
|
|
162
|
+
fontSize: 14,
|
|
163
|
+
fontWeight: 500,
|
|
164
|
+
cursor: "not-allowed"
|
|
165
|
+
},
|
|
166
|
+
error: { color: "#ef4444", fontSize: 13, marginTop: 8 },
|
|
167
|
+
doneIcon: { fontSize: 48, marginBottom: 12, textAlign: "center" }
|
|
168
|
+
};
|
|
169
|
+
function MfaSetup() {
|
|
170
|
+
const { authms } = (0, import_react3.useAuthmsContext)();
|
|
171
|
+
const [flow, setFlow] = (0, import_react2.useState)("setup");
|
|
172
|
+
const [data, setData] = (0, import_react2.useState)(null);
|
|
173
|
+
const [code, setCode] = (0, import_react2.useState)("");
|
|
174
|
+
const [loading, setLoading] = (0, import_react2.useState)(false);
|
|
175
|
+
const [error, setError] = (0, import_react2.useState)("");
|
|
176
|
+
async function handleGenerate() {
|
|
177
|
+
setLoading(true);
|
|
178
|
+
setError("");
|
|
179
|
+
try {
|
|
180
|
+
const result = await authms.api.post(
|
|
181
|
+
"/identity/api/v1/mfa/totp/generate"
|
|
182
|
+
);
|
|
183
|
+
setData(result);
|
|
184
|
+
setFlow("verify");
|
|
185
|
+
} catch (e) {
|
|
186
|
+
setError(e instanceof Error ? e.message : "Failed to generate MFA setup");
|
|
187
|
+
} finally {
|
|
188
|
+
setLoading(false);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function handleVerify() {
|
|
192
|
+
if (!code || !data) return;
|
|
193
|
+
setLoading(true);
|
|
194
|
+
setError("");
|
|
195
|
+
try {
|
|
196
|
+
await authms.api.post("/identity/api/v1/mfa/totp/verify", { code });
|
|
197
|
+
setFlow("done");
|
|
198
|
+
} catch (e) {
|
|
199
|
+
setError(e instanceof Error ? e.message : "Verification failed");
|
|
200
|
+
} finally {
|
|
201
|
+
setLoading(false);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (flow === "done") {
|
|
205
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles2.container, children: [
|
|
206
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.doneIcon, children: "\u2713" }),
|
|
207
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.title, children: "MFA Enabled" }),
|
|
208
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.subtitle, children: "Two-factor authentication has been set up successfully." }),
|
|
209
|
+
data?.backupCodes && data.backupCodes.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BackupCodes, { codes: data.backupCodes })
|
|
210
|
+
] });
|
|
211
|
+
}
|
|
212
|
+
if (flow === "verify" && data) {
|
|
213
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles2.container, children: [
|
|
214
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.title, children: "Verify Setup" }),
|
|
215
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.subtitle, children: "Scan the QR code with your authenticator app, then enter the 6-digit code." }),
|
|
216
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.qrPlaceholder, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
|
|
217
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { marginBottom: 8 }, children: "\u{1F50C}" }),
|
|
218
|
+
"QR Code",
|
|
219
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
|
|
220
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 11 }, children: "Use a qrcode library to render:" }),
|
|
221
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
|
|
222
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { fontSize: 11, fontFamily: "monospace" }, children: [
|
|
223
|
+
data.qrCodeUri.substring(0, 60),
|
|
224
|
+
"..."
|
|
225
|
+
] })
|
|
226
|
+
] }) }),
|
|
227
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles2.secretBox, children: [
|
|
228
|
+
"Or enter manually: ",
|
|
229
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: data.secret })
|
|
230
|
+
] }),
|
|
231
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
232
|
+
"input",
|
|
233
|
+
{
|
|
234
|
+
style: styles2.input,
|
|
235
|
+
type: "text",
|
|
236
|
+
inputMode: "numeric",
|
|
237
|
+
maxLength: 6,
|
|
238
|
+
placeholder: "000000",
|
|
239
|
+
value: code,
|
|
240
|
+
onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
|
|
241
|
+
onKeyDown: (e) => {
|
|
242
|
+
if (e.key === "Enter") handleVerify();
|
|
243
|
+
},
|
|
244
|
+
autoFocus: true
|
|
245
|
+
}
|
|
246
|
+
),
|
|
247
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
248
|
+
"button",
|
|
249
|
+
{
|
|
250
|
+
style: loading || code.length !== 6 ? styles2.buttonDisabled : styles2.button,
|
|
251
|
+
onClick: handleVerify,
|
|
252
|
+
disabled: loading || code.length !== 6,
|
|
253
|
+
children: loading ? "Verifying..." : "Confirm"
|
|
254
|
+
}
|
|
255
|
+
),
|
|
256
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.error, children: error })
|
|
257
|
+
] });
|
|
258
|
+
}
|
|
259
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles2.container, children: [
|
|
260
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.title, children: "Set Up Two-Factor Authentication" }),
|
|
261
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.subtitle, children: "Add an extra layer of security to your account by enabling time-based one-time passwords (TOTP)." }),
|
|
262
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
263
|
+
"button",
|
|
264
|
+
{
|
|
265
|
+
style: loading ? styles2.buttonDisabled : styles2.button,
|
|
266
|
+
onClick: handleGenerate,
|
|
267
|
+
disabled: loading,
|
|
268
|
+
children: loading ? "Generating..." : "Enable Authenticator App"
|
|
269
|
+
}
|
|
270
|
+
),
|
|
271
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles2.error, children: error })
|
|
272
|
+
] });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/MfaGuard.tsx
|
|
276
|
+
var import_react4 = require("react");
|
|
277
|
+
var import_react5 = require("@authms/react");
|
|
278
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
279
|
+
var styles3 = {
|
|
280
|
+
overlay: {
|
|
281
|
+
display: "flex",
|
|
282
|
+
alignItems: "center",
|
|
283
|
+
justifyContent: "center",
|
|
284
|
+
minHeight: 300,
|
|
285
|
+
padding: 24
|
|
286
|
+
},
|
|
287
|
+
card: { maxWidth: 380, width: "100%", textAlign: "center" },
|
|
288
|
+
title: { fontSize: 18, fontWeight: 600, marginBottom: 8 },
|
|
289
|
+
subtitle: { fontSize: 14, color: "#6b7280", marginBottom: 20 },
|
|
290
|
+
input: {
|
|
291
|
+
width: "100%",
|
|
292
|
+
padding: "10px 12px",
|
|
293
|
+
fontSize: 20,
|
|
294
|
+
textAlign: "center",
|
|
295
|
+
letterSpacing: 8,
|
|
296
|
+
border: "1px solid #d1d5db",
|
|
297
|
+
borderRadius: 6,
|
|
298
|
+
outline: "none",
|
|
299
|
+
boxSizing: "border-box"
|
|
300
|
+
},
|
|
301
|
+
button: {
|
|
302
|
+
width: "100%",
|
|
303
|
+
padding: "10px 0",
|
|
304
|
+
marginTop: 12,
|
|
305
|
+
background: "#4f46e5",
|
|
306
|
+
color: "#fff",
|
|
307
|
+
border: "none",
|
|
308
|
+
borderRadius: 6,
|
|
309
|
+
fontSize: 14,
|
|
310
|
+
fontWeight: 500,
|
|
311
|
+
cursor: "pointer"
|
|
312
|
+
},
|
|
313
|
+
buttonDisabled: {
|
|
314
|
+
width: "100%",
|
|
315
|
+
padding: "10px 0",
|
|
316
|
+
marginTop: 12,
|
|
317
|
+
background: "#9ca3af",
|
|
318
|
+
color: "#fff",
|
|
319
|
+
border: "none",
|
|
320
|
+
borderRadius: 6,
|
|
321
|
+
fontSize: 14,
|
|
322
|
+
fontWeight: 500,
|
|
323
|
+
cursor: "not-allowed"
|
|
324
|
+
},
|
|
325
|
+
error: { color: "#ef4444", fontSize: 13, marginTop: 8 },
|
|
326
|
+
loading: { fontSize: 14, color: "#9ca3af" }
|
|
327
|
+
};
|
|
328
|
+
function MfaGuard({ children }) {
|
|
329
|
+
const { authms } = (0, import_react5.useAuthmsContext)();
|
|
330
|
+
const [status, setStatus] = (0, import_react4.useState)(null);
|
|
331
|
+
const [busy, setBusy] = (0, import_react4.useState)(true);
|
|
332
|
+
const [code, setCode] = (0, import_react4.useState)("");
|
|
333
|
+
const [loading, setLoading] = (0, import_react4.useState)(false);
|
|
334
|
+
const [error, setError] = (0, import_react4.useState)("");
|
|
335
|
+
(0, import_react4.useEffect)(() => {
|
|
336
|
+
let cancelled = false;
|
|
337
|
+
authms.api.get("/identity/api/v1/mfa/status").then((s) => {
|
|
338
|
+
if (!cancelled) setStatus(s);
|
|
339
|
+
}).catch(() => {
|
|
340
|
+
if (!cancelled) setStatus({ enrolled: false, challenged: false });
|
|
341
|
+
}).finally(() => {
|
|
342
|
+
if (!cancelled) setBusy(false);
|
|
343
|
+
});
|
|
344
|
+
return () => {
|
|
345
|
+
cancelled = true;
|
|
346
|
+
};
|
|
347
|
+
}, [authms]);
|
|
348
|
+
if (busy) {
|
|
349
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles3.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles3.loading, children: "Loading..." }) });
|
|
350
|
+
}
|
|
351
|
+
if (!status?.enrolled) {
|
|
352
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children });
|
|
353
|
+
}
|
|
354
|
+
if (status.challenged) {
|
|
355
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children });
|
|
356
|
+
}
|
|
357
|
+
async function handleChallenge() {
|
|
358
|
+
if (!code) return;
|
|
359
|
+
setLoading(true);
|
|
360
|
+
setError("");
|
|
361
|
+
try {
|
|
362
|
+
await authms.api.post("/identity/api/v1/mfa/challenge/verify", { code });
|
|
363
|
+
setStatus((prev) => prev ? { ...prev, challenged: true } : prev);
|
|
364
|
+
} catch (e) {
|
|
365
|
+
setError(e instanceof Error ? e.message : "Challenge failed");
|
|
366
|
+
} finally {
|
|
367
|
+
setLoading(false);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles3.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles3.card, children: [
|
|
371
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles3.title, children: "Two-Factor Authentication" }),
|
|
372
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles3.subtitle, children: "Enter the 6-digit code from your authenticator app to continue." }),
|
|
373
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
374
|
+
"input",
|
|
375
|
+
{
|
|
376
|
+
style: styles3.input,
|
|
377
|
+
type: "text",
|
|
378
|
+
inputMode: "numeric",
|
|
379
|
+
maxLength: 6,
|
|
380
|
+
placeholder: "000000",
|
|
381
|
+
value: code,
|
|
382
|
+
onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
|
|
383
|
+
onKeyDown: (e) => {
|
|
384
|
+
if (e.key === "Enter") handleChallenge();
|
|
385
|
+
},
|
|
386
|
+
autoFocus: true
|
|
387
|
+
}
|
|
388
|
+
),
|
|
389
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
390
|
+
"button",
|
|
391
|
+
{
|
|
392
|
+
style: loading || code.length !== 6 ? styles3.buttonDisabled : styles3.button,
|
|
393
|
+
onClick: handleChallenge,
|
|
394
|
+
disabled: loading || code.length !== 6,
|
|
395
|
+
children: loading ? "Verifying..." : "Verify"
|
|
396
|
+
}
|
|
397
|
+
),
|
|
398
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: styles3.error, children: error })
|
|
399
|
+
] }) });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/mfa.ts
|
|
403
|
+
function mfaPlugin() {
|
|
404
|
+
return {
|
|
405
|
+
name: "@authms/plugin-mfa",
|
|
406
|
+
version: "0.1.0",
|
|
407
|
+
install(_core) {
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
412
|
+
0 && (module.exports = {
|
|
413
|
+
BackupCodes,
|
|
414
|
+
MfaGuard,
|
|
415
|
+
MfaSetup,
|
|
416
|
+
mfaPlugin
|
|
417
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// src/MfaSetup.tsx
|
|
2
|
+
import { useState as useState2 } from "react";
|
|
3
|
+
import { useAuthmsContext } from "@authms/react";
|
|
4
|
+
|
|
5
|
+
// src/BackupCodes.tsx
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
var styles = {
|
|
9
|
+
wrapper: { marginTop: 20 },
|
|
10
|
+
warning: {
|
|
11
|
+
background: "#fef3c7",
|
|
12
|
+
border: "1px solid #f59e0b",
|
|
13
|
+
borderRadius: 6,
|
|
14
|
+
padding: "10px 14px",
|
|
15
|
+
marginBottom: 14,
|
|
16
|
+
fontSize: 13,
|
|
17
|
+
color: "#92400e"
|
|
18
|
+
},
|
|
19
|
+
codeList: {
|
|
20
|
+
background: "#1f2937",
|
|
21
|
+
color: "#e5e7eb",
|
|
22
|
+
borderRadius: 8,
|
|
23
|
+
padding: "14px 18px",
|
|
24
|
+
fontFamily: "monospace",
|
|
25
|
+
fontSize: 14,
|
|
26
|
+
lineHeight: 2,
|
|
27
|
+
display: "grid",
|
|
28
|
+
gridTemplateColumns: "1fr 1fr",
|
|
29
|
+
gap: "0 16px",
|
|
30
|
+
marginBottom: 14
|
|
31
|
+
},
|
|
32
|
+
button: {
|
|
33
|
+
width: "100%",
|
|
34
|
+
padding: "8px 0",
|
|
35
|
+
background: "#fff",
|
|
36
|
+
color: "#374151",
|
|
37
|
+
border: "1px solid #d1d5db",
|
|
38
|
+
borderRadius: 6,
|
|
39
|
+
fontSize: 13,
|
|
40
|
+
fontWeight: 500,
|
|
41
|
+
cursor: "pointer"
|
|
42
|
+
},
|
|
43
|
+
copied: {
|
|
44
|
+
width: "100%",
|
|
45
|
+
padding: "8px 0",
|
|
46
|
+
background: "#ecfdf5",
|
|
47
|
+
color: "#065f46",
|
|
48
|
+
border: "1px solid #6ee7b7",
|
|
49
|
+
borderRadius: 6,
|
|
50
|
+
fontSize: 13,
|
|
51
|
+
fontWeight: 500
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function BackupCodes({ codes }) {
|
|
55
|
+
const [copied, setCopied] = useState(false);
|
|
56
|
+
async function handleCopy() {
|
|
57
|
+
await navigator.clipboard.writeText(codes.join("\n"));
|
|
58
|
+
setCopied(true);
|
|
59
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
60
|
+
}
|
|
61
|
+
return /* @__PURE__ */ jsxs("div", { style: styles.wrapper, children: [
|
|
62
|
+
/* @__PURE__ */ jsx("div", { style: styles.warning, children: "\u26A0 Store these recovery codes in a safe place. Each code can only be used once. If you lose access to your authenticator app, these codes are the only way to recover your account." }),
|
|
63
|
+
/* @__PURE__ */ jsx("div", { style: styles.codeList, children: codes.map((c, i) => /* @__PURE__ */ jsx("span", { children: c }, i)) }),
|
|
64
|
+
/* @__PURE__ */ jsx(
|
|
65
|
+
"button",
|
|
66
|
+
{
|
|
67
|
+
style: copied ? styles.copied : styles.button,
|
|
68
|
+
onClick: handleCopy,
|
|
69
|
+
children: copied ? "Copied!" : "Copy All Codes"
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
] });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/MfaSetup.tsx
|
|
76
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
77
|
+
var styles2 = {
|
|
78
|
+
container: { maxWidth: 420, margin: "0 auto", padding: 24 },
|
|
79
|
+
title: { fontSize: 20, fontWeight: 600, marginBottom: 8 },
|
|
80
|
+
subtitle: { fontSize: 14, color: "#6b7280", marginBottom: 20 },
|
|
81
|
+
qrPlaceholder: {
|
|
82
|
+
width: 200,
|
|
83
|
+
height: 200,
|
|
84
|
+
margin: "0 auto 16px",
|
|
85
|
+
background: "#f3f4f6",
|
|
86
|
+
borderRadius: 8,
|
|
87
|
+
display: "flex",
|
|
88
|
+
alignItems: "center",
|
|
89
|
+
justifyContent: "center",
|
|
90
|
+
fontSize: 13,
|
|
91
|
+
color: "#9ca3af",
|
|
92
|
+
textAlign: "center"
|
|
93
|
+
},
|
|
94
|
+
secretBox: {
|
|
95
|
+
background: "#f9fafb",
|
|
96
|
+
border: "1px solid #e5e7eb",
|
|
97
|
+
borderRadius: 6,
|
|
98
|
+
padding: "10px 14px",
|
|
99
|
+
marginBottom: 20,
|
|
100
|
+
fontFamily: "monospace",
|
|
101
|
+
fontSize: 14,
|
|
102
|
+
wordBreak: "break-all"
|
|
103
|
+
},
|
|
104
|
+
input: {
|
|
105
|
+
width: "100%",
|
|
106
|
+
padding: "10px 12px",
|
|
107
|
+
fontSize: 16,
|
|
108
|
+
border: "1px solid #d1d5db",
|
|
109
|
+
borderRadius: 6,
|
|
110
|
+
outline: "none",
|
|
111
|
+
boxSizing: "border-box"
|
|
112
|
+
},
|
|
113
|
+
button: {
|
|
114
|
+
width: "100%",
|
|
115
|
+
padding: "10px 0",
|
|
116
|
+
marginTop: 12,
|
|
117
|
+
background: "#4f46e5",
|
|
118
|
+
color: "#fff",
|
|
119
|
+
border: "none",
|
|
120
|
+
borderRadius: 6,
|
|
121
|
+
fontSize: 14,
|
|
122
|
+
fontWeight: 500,
|
|
123
|
+
cursor: "pointer"
|
|
124
|
+
},
|
|
125
|
+
buttonDisabled: {
|
|
126
|
+
width: "100%",
|
|
127
|
+
padding: "10px 0",
|
|
128
|
+
marginTop: 12,
|
|
129
|
+
background: "#9ca3af",
|
|
130
|
+
color: "#fff",
|
|
131
|
+
border: "none",
|
|
132
|
+
borderRadius: 6,
|
|
133
|
+
fontSize: 14,
|
|
134
|
+
fontWeight: 500,
|
|
135
|
+
cursor: "not-allowed"
|
|
136
|
+
},
|
|
137
|
+
error: { color: "#ef4444", fontSize: 13, marginTop: 8 },
|
|
138
|
+
doneIcon: { fontSize: 48, marginBottom: 12, textAlign: "center" }
|
|
139
|
+
};
|
|
140
|
+
function MfaSetup() {
|
|
141
|
+
const { authms } = useAuthmsContext();
|
|
142
|
+
const [flow, setFlow] = useState2("setup");
|
|
143
|
+
const [data, setData] = useState2(null);
|
|
144
|
+
const [code, setCode] = useState2("");
|
|
145
|
+
const [loading, setLoading] = useState2(false);
|
|
146
|
+
const [error, setError] = useState2("");
|
|
147
|
+
async function handleGenerate() {
|
|
148
|
+
setLoading(true);
|
|
149
|
+
setError("");
|
|
150
|
+
try {
|
|
151
|
+
const result = await authms.api.post(
|
|
152
|
+
"/identity/api/v1/mfa/totp/generate"
|
|
153
|
+
);
|
|
154
|
+
setData(result);
|
|
155
|
+
setFlow("verify");
|
|
156
|
+
} catch (e) {
|
|
157
|
+
setError(e instanceof Error ? e.message : "Failed to generate MFA setup");
|
|
158
|
+
} finally {
|
|
159
|
+
setLoading(false);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function handleVerify() {
|
|
163
|
+
if (!code || !data) return;
|
|
164
|
+
setLoading(true);
|
|
165
|
+
setError("");
|
|
166
|
+
try {
|
|
167
|
+
await authms.api.post("/identity/api/v1/mfa/totp/verify", { code });
|
|
168
|
+
setFlow("done");
|
|
169
|
+
} catch (e) {
|
|
170
|
+
setError(e instanceof Error ? e.message : "Verification failed");
|
|
171
|
+
} finally {
|
|
172
|
+
setLoading(false);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (flow === "done") {
|
|
176
|
+
return /* @__PURE__ */ jsxs2("div", { style: styles2.container, children: [
|
|
177
|
+
/* @__PURE__ */ jsx2("div", { style: styles2.doneIcon, children: "\u2713" }),
|
|
178
|
+
/* @__PURE__ */ jsx2("div", { style: styles2.title, children: "MFA Enabled" }),
|
|
179
|
+
/* @__PURE__ */ jsx2("div", { style: styles2.subtitle, children: "Two-factor authentication has been set up successfully." }),
|
|
180
|
+
data?.backupCodes && data.backupCodes.length > 0 && /* @__PURE__ */ jsx2(BackupCodes, { codes: data.backupCodes })
|
|
181
|
+
] });
|
|
182
|
+
}
|
|
183
|
+
if (flow === "verify" && data) {
|
|
184
|
+
return /* @__PURE__ */ jsxs2("div", { style: styles2.container, children: [
|
|
185
|
+
/* @__PURE__ */ jsx2("div", { style: styles2.title, children: "Verify Setup" }),
|
|
186
|
+
/* @__PURE__ */ jsx2("div", { style: styles2.subtitle, children: "Scan the QR code with your authenticator app, then enter the 6-digit code." }),
|
|
187
|
+
/* @__PURE__ */ jsx2("div", { style: styles2.qrPlaceholder, children: /* @__PURE__ */ jsxs2("div", { children: [
|
|
188
|
+
/* @__PURE__ */ jsx2("div", { style: { marginBottom: 8 }, children: "\u{1F50C}" }),
|
|
189
|
+
"QR Code",
|
|
190
|
+
/* @__PURE__ */ jsx2("br", {}),
|
|
191
|
+
/* @__PURE__ */ jsx2("span", { style: { fontSize: 11 }, children: "Use a qrcode library to render:" }),
|
|
192
|
+
/* @__PURE__ */ jsx2("br", {}),
|
|
193
|
+
/* @__PURE__ */ jsxs2("span", { style: { fontSize: 11, fontFamily: "monospace" }, children: [
|
|
194
|
+
data.qrCodeUri.substring(0, 60),
|
|
195
|
+
"..."
|
|
196
|
+
] })
|
|
197
|
+
] }) }),
|
|
198
|
+
/* @__PURE__ */ jsxs2("div", { style: styles2.secretBox, children: [
|
|
199
|
+
"Or enter manually: ",
|
|
200
|
+
/* @__PURE__ */ jsx2("strong", { children: data.secret })
|
|
201
|
+
] }),
|
|
202
|
+
/* @__PURE__ */ jsx2(
|
|
203
|
+
"input",
|
|
204
|
+
{
|
|
205
|
+
style: styles2.input,
|
|
206
|
+
type: "text",
|
|
207
|
+
inputMode: "numeric",
|
|
208
|
+
maxLength: 6,
|
|
209
|
+
placeholder: "000000",
|
|
210
|
+
value: code,
|
|
211
|
+
onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
|
|
212
|
+
onKeyDown: (e) => {
|
|
213
|
+
if (e.key === "Enter") handleVerify();
|
|
214
|
+
},
|
|
215
|
+
autoFocus: true
|
|
216
|
+
}
|
|
217
|
+
),
|
|
218
|
+
/* @__PURE__ */ jsx2(
|
|
219
|
+
"button",
|
|
220
|
+
{
|
|
221
|
+
style: loading || code.length !== 6 ? styles2.buttonDisabled : styles2.button,
|
|
222
|
+
onClick: handleVerify,
|
|
223
|
+
disabled: loading || code.length !== 6,
|
|
224
|
+
children: loading ? "Verifying..." : "Confirm"
|
|
225
|
+
}
|
|
226
|
+
),
|
|
227
|
+
error && /* @__PURE__ */ jsx2("div", { style: styles2.error, children: error })
|
|
228
|
+
] });
|
|
229
|
+
}
|
|
230
|
+
return /* @__PURE__ */ jsxs2("div", { style: styles2.container, children: [
|
|
231
|
+
/* @__PURE__ */ jsx2("div", { style: styles2.title, children: "Set Up Two-Factor Authentication" }),
|
|
232
|
+
/* @__PURE__ */ jsx2("div", { style: styles2.subtitle, children: "Add an extra layer of security to your account by enabling time-based one-time passwords (TOTP)." }),
|
|
233
|
+
/* @__PURE__ */ jsx2(
|
|
234
|
+
"button",
|
|
235
|
+
{
|
|
236
|
+
style: loading ? styles2.buttonDisabled : styles2.button,
|
|
237
|
+
onClick: handleGenerate,
|
|
238
|
+
disabled: loading,
|
|
239
|
+
children: loading ? "Generating..." : "Enable Authenticator App"
|
|
240
|
+
}
|
|
241
|
+
),
|
|
242
|
+
error && /* @__PURE__ */ jsx2("div", { style: styles2.error, children: error })
|
|
243
|
+
] });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/MfaGuard.tsx
|
|
247
|
+
import { useState as useState3, useEffect } from "react";
|
|
248
|
+
import { useAuthmsContext as useAuthmsContext2 } from "@authms/react";
|
|
249
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
250
|
+
var styles3 = {
|
|
251
|
+
overlay: {
|
|
252
|
+
display: "flex",
|
|
253
|
+
alignItems: "center",
|
|
254
|
+
justifyContent: "center",
|
|
255
|
+
minHeight: 300,
|
|
256
|
+
padding: 24
|
|
257
|
+
},
|
|
258
|
+
card: { maxWidth: 380, width: "100%", textAlign: "center" },
|
|
259
|
+
title: { fontSize: 18, fontWeight: 600, marginBottom: 8 },
|
|
260
|
+
subtitle: { fontSize: 14, color: "#6b7280", marginBottom: 20 },
|
|
261
|
+
input: {
|
|
262
|
+
width: "100%",
|
|
263
|
+
padding: "10px 12px",
|
|
264
|
+
fontSize: 20,
|
|
265
|
+
textAlign: "center",
|
|
266
|
+
letterSpacing: 8,
|
|
267
|
+
border: "1px solid #d1d5db",
|
|
268
|
+
borderRadius: 6,
|
|
269
|
+
outline: "none",
|
|
270
|
+
boxSizing: "border-box"
|
|
271
|
+
},
|
|
272
|
+
button: {
|
|
273
|
+
width: "100%",
|
|
274
|
+
padding: "10px 0",
|
|
275
|
+
marginTop: 12,
|
|
276
|
+
background: "#4f46e5",
|
|
277
|
+
color: "#fff",
|
|
278
|
+
border: "none",
|
|
279
|
+
borderRadius: 6,
|
|
280
|
+
fontSize: 14,
|
|
281
|
+
fontWeight: 500,
|
|
282
|
+
cursor: "pointer"
|
|
283
|
+
},
|
|
284
|
+
buttonDisabled: {
|
|
285
|
+
width: "100%",
|
|
286
|
+
padding: "10px 0",
|
|
287
|
+
marginTop: 12,
|
|
288
|
+
background: "#9ca3af",
|
|
289
|
+
color: "#fff",
|
|
290
|
+
border: "none",
|
|
291
|
+
borderRadius: 6,
|
|
292
|
+
fontSize: 14,
|
|
293
|
+
fontWeight: 500,
|
|
294
|
+
cursor: "not-allowed"
|
|
295
|
+
},
|
|
296
|
+
error: { color: "#ef4444", fontSize: 13, marginTop: 8 },
|
|
297
|
+
loading: { fontSize: 14, color: "#9ca3af" }
|
|
298
|
+
};
|
|
299
|
+
function MfaGuard({ children }) {
|
|
300
|
+
const { authms } = useAuthmsContext2();
|
|
301
|
+
const [status, setStatus] = useState3(null);
|
|
302
|
+
const [busy, setBusy] = useState3(true);
|
|
303
|
+
const [code, setCode] = useState3("");
|
|
304
|
+
const [loading, setLoading] = useState3(false);
|
|
305
|
+
const [error, setError] = useState3("");
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
let cancelled = false;
|
|
308
|
+
authms.api.get("/identity/api/v1/mfa/status").then((s) => {
|
|
309
|
+
if (!cancelled) setStatus(s);
|
|
310
|
+
}).catch(() => {
|
|
311
|
+
if (!cancelled) setStatus({ enrolled: false, challenged: false });
|
|
312
|
+
}).finally(() => {
|
|
313
|
+
if (!cancelled) setBusy(false);
|
|
314
|
+
});
|
|
315
|
+
return () => {
|
|
316
|
+
cancelled = true;
|
|
317
|
+
};
|
|
318
|
+
}, [authms]);
|
|
319
|
+
if (busy) {
|
|
320
|
+
return /* @__PURE__ */ jsx3("div", { style: styles3.overlay, children: /* @__PURE__ */ jsx3("div", { style: styles3.loading, children: "Loading..." }) });
|
|
321
|
+
}
|
|
322
|
+
if (!status?.enrolled) {
|
|
323
|
+
return /* @__PURE__ */ jsx3(Fragment, { children });
|
|
324
|
+
}
|
|
325
|
+
if (status.challenged) {
|
|
326
|
+
return /* @__PURE__ */ jsx3(Fragment, { children });
|
|
327
|
+
}
|
|
328
|
+
async function handleChallenge() {
|
|
329
|
+
if (!code) return;
|
|
330
|
+
setLoading(true);
|
|
331
|
+
setError("");
|
|
332
|
+
try {
|
|
333
|
+
await authms.api.post("/identity/api/v1/mfa/challenge/verify", { code });
|
|
334
|
+
setStatus((prev) => prev ? { ...prev, challenged: true } : prev);
|
|
335
|
+
} catch (e) {
|
|
336
|
+
setError(e instanceof Error ? e.message : "Challenge failed");
|
|
337
|
+
} finally {
|
|
338
|
+
setLoading(false);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return /* @__PURE__ */ jsx3("div", { style: styles3.overlay, children: /* @__PURE__ */ jsxs3("div", { style: styles3.card, children: [
|
|
342
|
+
/* @__PURE__ */ jsx3("div", { style: styles3.title, children: "Two-Factor Authentication" }),
|
|
343
|
+
/* @__PURE__ */ jsx3("div", { style: styles3.subtitle, children: "Enter the 6-digit code from your authenticator app to continue." }),
|
|
344
|
+
/* @__PURE__ */ jsx3(
|
|
345
|
+
"input",
|
|
346
|
+
{
|
|
347
|
+
style: styles3.input,
|
|
348
|
+
type: "text",
|
|
349
|
+
inputMode: "numeric",
|
|
350
|
+
maxLength: 6,
|
|
351
|
+
placeholder: "000000",
|
|
352
|
+
value: code,
|
|
353
|
+
onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
|
|
354
|
+
onKeyDown: (e) => {
|
|
355
|
+
if (e.key === "Enter") handleChallenge();
|
|
356
|
+
},
|
|
357
|
+
autoFocus: true
|
|
358
|
+
}
|
|
359
|
+
),
|
|
360
|
+
/* @__PURE__ */ jsx3(
|
|
361
|
+
"button",
|
|
362
|
+
{
|
|
363
|
+
style: loading || code.length !== 6 ? styles3.buttonDisabled : styles3.button,
|
|
364
|
+
onClick: handleChallenge,
|
|
365
|
+
disabled: loading || code.length !== 6,
|
|
366
|
+
children: loading ? "Verifying..." : "Verify"
|
|
367
|
+
}
|
|
368
|
+
),
|
|
369
|
+
error && /* @__PURE__ */ jsx3("div", { style: styles3.error, children: error })
|
|
370
|
+
] }) });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/mfa.ts
|
|
374
|
+
function mfaPlugin() {
|
|
375
|
+
return {
|
|
376
|
+
name: "@authms/plugin-mfa",
|
|
377
|
+
version: "0.1.0",
|
|
378
|
+
install(_core) {
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
export {
|
|
383
|
+
BackupCodes,
|
|
384
|
+
MfaGuard,
|
|
385
|
+
MfaSetup,
|
|
386
|
+
mfaPlugin
|
|
387
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@autional/plugin-mfa",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "AuthMS MFA Plugin — Multi-Factor Authentication UI components and flows for @autional/react",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean --external react --external react-dom --external @autional/core --external @autional/react",
|
|
18
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch --external react --external react-dom --external @autional/core --external @autional/react",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"clean": "rimraf dist"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"auth",
|
|
28
|
+
"authentication",
|
|
29
|
+
"mfa",
|
|
30
|
+
"totp",
|
|
31
|
+
"2fa",
|
|
32
|
+
"authms",
|
|
33
|
+
"plugin"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@autional/core": ">=0.1.0",
|
|
38
|
+
"@autional/react": ">=0.1.0",
|
|
39
|
+
"react": ">=18.0.0",
|
|
40
|
+
"react-dom": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@autional/core": ">=0.1.0",
|
|
44
|
+
"@autional/react": ">=0.1.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@autional/tsconfig": ">=0.1.0",
|
|
48
|
+
"@types/react": "^19.0.0",
|
|
49
|
+
"@types/react-dom": "^19.0.0",
|
|
50
|
+
"react": ">=18.0.0",
|
|
51
|
+
"react-dom": "^19.0.0",
|
|
52
|
+
"rimraf": "^5.0.0",
|
|
53
|
+
"tsup": "^8.4.0",
|
|
54
|
+
"typescript": "^5.8.0",
|
|
55
|
+
"vitest": "^3.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const styles = {
|
|
4
|
+
wrapper: { marginTop: 20 } as React.CSSProperties,
|
|
5
|
+
warning: {
|
|
6
|
+
background: '#fef3c7', border: '1px solid #f59e0b',
|
|
7
|
+
borderRadius: 6, padding: '10px 14px', marginBottom: 14,
|
|
8
|
+
fontSize: 13, color: '#92400e',
|
|
9
|
+
} as React.CSSProperties,
|
|
10
|
+
codeList: {
|
|
11
|
+
background: '#1f2937', color: '#e5e7eb',
|
|
12
|
+
borderRadius: 8, padding: '14px 18px',
|
|
13
|
+
fontFamily: 'monospace', fontSize: 14, lineHeight: 2,
|
|
14
|
+
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 16px',
|
|
15
|
+
marginBottom: 14,
|
|
16
|
+
} as React.CSSProperties,
|
|
17
|
+
button: {
|
|
18
|
+
width: '100%', padding: '8px 0',
|
|
19
|
+
background: '#fff', color: '#374151', border: '1px solid #d1d5db',
|
|
20
|
+
borderRadius: 6, fontSize: 13, fontWeight: 500, cursor: 'pointer',
|
|
21
|
+
} as React.CSSProperties,
|
|
22
|
+
copied: {
|
|
23
|
+
width: '100%', padding: '8px 0',
|
|
24
|
+
background: '#ecfdf5', color: '#065f46', border: '1px solid #6ee7b7',
|
|
25
|
+
borderRadius: 6, fontSize: 13, fontWeight: 500,
|
|
26
|
+
} as React.CSSProperties,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function BackupCodes({ codes }: { codes: string[] }) {
|
|
30
|
+
const [copied, setCopied] = useState(false);
|
|
31
|
+
|
|
32
|
+
async function handleCopy() {
|
|
33
|
+
await navigator.clipboard.writeText(codes.join('\n'));
|
|
34
|
+
setCopied(true);
|
|
35
|
+
setTimeout(() => setCopied(false), 2000);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div style={styles.wrapper}>
|
|
40
|
+
<div style={styles.warning}>
|
|
41
|
+
⚠ Store these recovery codes in a safe place. Each code can only be
|
|
42
|
+
used once. If you lose access to your authenticator app, these codes are
|
|
43
|
+
the only way to recover your account.
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div style={styles.codeList}>
|
|
47
|
+
{codes.map((c, i) => (
|
|
48
|
+
<span key={i}>{c}</span>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<button
|
|
53
|
+
style={copied ? styles.copied : styles.button}
|
|
54
|
+
onClick={handleCopy}
|
|
55
|
+
>
|
|
56
|
+
{copied ? 'Copied!' : 'Copy All Codes'}
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
package/src/MfaGuard.tsx
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useState, useEffect, type ReactNode } from 'react';
|
|
2
|
+
import { useAuthmsContext } from '@authms/react';
|
|
3
|
+
|
|
4
|
+
interface MfaStatus {
|
|
5
|
+
enrolled: boolean;
|
|
6
|
+
challenged: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const styles = {
|
|
10
|
+
overlay: {
|
|
11
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
12
|
+
minHeight: 300, padding: 24,
|
|
13
|
+
} as React.CSSProperties,
|
|
14
|
+
card: { maxWidth: 380, width: '100%', textAlign: 'center' } as React.CSSProperties,
|
|
15
|
+
title: { fontSize: 18, fontWeight: 600, marginBottom: 8 } as React.CSSProperties,
|
|
16
|
+
subtitle: { fontSize: 14, color: '#6b7280', marginBottom: 20 } as React.CSSProperties,
|
|
17
|
+
input: {
|
|
18
|
+
width: '100%', padding: '10px 12px', fontSize: 20, textAlign: 'center',
|
|
19
|
+
letterSpacing: 8, border: '1px solid #d1d5db', borderRadius: 6, outline: 'none',
|
|
20
|
+
boxSizing: 'border-box',
|
|
21
|
+
} as React.CSSProperties,
|
|
22
|
+
button: {
|
|
23
|
+
width: '100%', padding: '10px 0', marginTop: 12,
|
|
24
|
+
background: '#4f46e5', color: '#fff', border: 'none',
|
|
25
|
+
borderRadius: 6, fontSize: 14, fontWeight: 500, cursor: 'pointer',
|
|
26
|
+
} as React.CSSProperties,
|
|
27
|
+
buttonDisabled: {
|
|
28
|
+
width: '100%', padding: '10px 0', marginTop: 12,
|
|
29
|
+
background: '#9ca3af', color: '#fff', border: 'none',
|
|
30
|
+
borderRadius: 6, fontSize: 14, fontWeight: 500, cursor: 'not-allowed',
|
|
31
|
+
} as React.CSSProperties,
|
|
32
|
+
error: { color: '#ef4444', fontSize: 13, marginTop: 8 } as React.CSSProperties,
|
|
33
|
+
loading: { fontSize: 14, color: '#9ca3af' } as React.CSSProperties,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function MfaGuard({ children }: { children: ReactNode }) {
|
|
37
|
+
const { authms } = useAuthmsContext();
|
|
38
|
+
const [status, setStatus] = useState<MfaStatus | null>(null);
|
|
39
|
+
const [busy, setBusy] = useState(true);
|
|
40
|
+
const [code, setCode] = useState('');
|
|
41
|
+
const [loading, setLoading] = useState(false);
|
|
42
|
+
const [error, setError] = useState('');
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let cancelled = false;
|
|
46
|
+
authms.api.get<MfaStatus>('/identity/api/v1/mfa/status')
|
|
47
|
+
.then((s) => { if (!cancelled) setStatus(s); })
|
|
48
|
+
.catch(() => { if (!cancelled) setStatus({ enrolled: false, challenged: false }); })
|
|
49
|
+
.finally(() => { if (!cancelled) setBusy(false); });
|
|
50
|
+
return () => { cancelled = true; };
|
|
51
|
+
}, [authms]);
|
|
52
|
+
|
|
53
|
+
if (busy) {
|
|
54
|
+
return <div style={styles.overlay}><div style={styles.loading}>Loading...</div></div>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!status?.enrolled) {
|
|
58
|
+
return <>{children}</>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (status.challenged) {
|
|
62
|
+
return <>{children}</>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function handleChallenge() {
|
|
66
|
+
if (!code) return;
|
|
67
|
+
setLoading(true);
|
|
68
|
+
setError('');
|
|
69
|
+
try {
|
|
70
|
+
await authms.api.post('/identity/api/v1/mfa/challenge/verify', { code });
|
|
71
|
+
setStatus((prev) => prev ? { ...prev, challenged: true } : prev);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
setError(e instanceof Error ? e.message : 'Challenge failed');
|
|
74
|
+
} finally {
|
|
75
|
+
setLoading(false);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div style={styles.overlay}>
|
|
81
|
+
<div style={styles.card}>
|
|
82
|
+
<div style={styles.title}>Two-Factor Authentication</div>
|
|
83
|
+
<div style={styles.subtitle}>
|
|
84
|
+
Enter the 6-digit code from your authenticator app to continue.
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<input
|
|
88
|
+
style={styles.input}
|
|
89
|
+
type="text"
|
|
90
|
+
inputMode="numeric"
|
|
91
|
+
maxLength={6}
|
|
92
|
+
placeholder="000000"
|
|
93
|
+
value={code}
|
|
94
|
+
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
|
95
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleChallenge(); }}
|
|
96
|
+
autoFocus
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
<button
|
|
100
|
+
style={loading || code.length !== 6 ? styles.buttonDisabled : styles.button}
|
|
101
|
+
onClick={handleChallenge}
|
|
102
|
+
disabled={loading || code.length !== 6}
|
|
103
|
+
>
|
|
104
|
+
{loading ? 'Verifying...' : 'Verify'}
|
|
105
|
+
</button>
|
|
106
|
+
|
|
107
|
+
{error && <div style={styles.error}>{error}</div>}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
package/src/MfaSetup.tsx
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useAuthmsContext } from '@authms/react';
|
|
3
|
+
import { BackupCodes } from './BackupCodes';
|
|
4
|
+
|
|
5
|
+
type FlowState = 'setup' | 'verify' | 'done';
|
|
6
|
+
|
|
7
|
+
interface TotpSetupData {
|
|
8
|
+
secret: string;
|
|
9
|
+
qrCodeUri: string;
|
|
10
|
+
backupCodes: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const styles = {
|
|
14
|
+
container: { maxWidth: 420, margin: '0 auto', padding: 24 } as React.CSSProperties,
|
|
15
|
+
title: { fontSize: 20, fontWeight: 600, marginBottom: 8 } as React.CSSProperties,
|
|
16
|
+
subtitle: { fontSize: 14, color: '#6b7280', marginBottom: 20 } as React.CSSProperties,
|
|
17
|
+
qrPlaceholder: {
|
|
18
|
+
width: 200, height: 200, margin: '0 auto 16px',
|
|
19
|
+
background: '#f3f4f6', borderRadius: 8,
|
|
20
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
21
|
+
fontSize: 13, color: '#9ca3af', textAlign: 'center',
|
|
22
|
+
} as React.CSSProperties,
|
|
23
|
+
secretBox: {
|
|
24
|
+
background: '#f9fafb', border: '1px solid #e5e7eb',
|
|
25
|
+
borderRadius: 6, padding: '10px 14px', marginBottom: 20,
|
|
26
|
+
fontFamily: 'monospace', fontSize: 14, wordBreak: 'break-all',
|
|
27
|
+
} as React.CSSProperties,
|
|
28
|
+
input: {
|
|
29
|
+
width: '100%', padding: '10px 12px', fontSize: 16,
|
|
30
|
+
border: '1px solid #d1d5db', borderRadius: 6, outline: 'none',
|
|
31
|
+
boxSizing: 'border-box',
|
|
32
|
+
} as React.CSSProperties,
|
|
33
|
+
button: {
|
|
34
|
+
width: '100%', padding: '10px 0', marginTop: 12,
|
|
35
|
+
background: '#4f46e5', color: '#fff', border: 'none',
|
|
36
|
+
borderRadius: 6, fontSize: 14, fontWeight: 500, cursor: 'pointer',
|
|
37
|
+
} as React.CSSProperties,
|
|
38
|
+
buttonDisabled: {
|
|
39
|
+
width: '100%', padding: '10px 0', marginTop: 12,
|
|
40
|
+
background: '#9ca3af', color: '#fff', border: 'none',
|
|
41
|
+
borderRadius: 6, fontSize: 14, fontWeight: 500, cursor: 'not-allowed',
|
|
42
|
+
} as React.CSSProperties,
|
|
43
|
+
error: { color: '#ef4444', fontSize: 13, marginTop: 8 } as React.CSSProperties,
|
|
44
|
+
doneIcon: { fontSize: 48, marginBottom: 12, textAlign: 'center' } as React.CSSProperties,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function MfaSetup() {
|
|
48
|
+
const { authms } = useAuthmsContext();
|
|
49
|
+
const [flow, setFlow] = useState<FlowState>('setup');
|
|
50
|
+
const [data, setData] = useState<TotpSetupData | null>(null);
|
|
51
|
+
const [code, setCode] = useState('');
|
|
52
|
+
const [loading, setLoading] = useState(false);
|
|
53
|
+
const [error, setError] = useState('');
|
|
54
|
+
|
|
55
|
+
async function handleGenerate() {
|
|
56
|
+
setLoading(true);
|
|
57
|
+
setError('');
|
|
58
|
+
try {
|
|
59
|
+
const result = await authms.api.post<TotpSetupData>(
|
|
60
|
+
'/identity/api/v1/mfa/totp/generate',
|
|
61
|
+
);
|
|
62
|
+
setData(result);
|
|
63
|
+
setFlow('verify');
|
|
64
|
+
} catch (e) {
|
|
65
|
+
setError(e instanceof Error ? e.message : 'Failed to generate MFA setup');
|
|
66
|
+
} finally {
|
|
67
|
+
setLoading(false);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function handleVerify() {
|
|
72
|
+
if (!code || !data) return;
|
|
73
|
+
setLoading(true);
|
|
74
|
+
setError('');
|
|
75
|
+
try {
|
|
76
|
+
await authms.api.post('/identity/api/v1/mfa/totp/verify', { code });
|
|
77
|
+
setFlow('done');
|
|
78
|
+
} catch (e) {
|
|
79
|
+
setError(e instanceof Error ? e.message : 'Verification failed');
|
|
80
|
+
} finally {
|
|
81
|
+
setLoading(false);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (flow === 'done') {
|
|
86
|
+
return (
|
|
87
|
+
<div style={styles.container}>
|
|
88
|
+
<div style={styles.doneIcon}>✓</div>
|
|
89
|
+
<div style={styles.title}>MFA Enabled</div>
|
|
90
|
+
<div style={styles.subtitle}>
|
|
91
|
+
Two-factor authentication has been set up successfully.
|
|
92
|
+
</div>
|
|
93
|
+
{data?.backupCodes && data.backupCodes.length > 0 && (
|
|
94
|
+
<BackupCodes codes={data.backupCodes} />
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (flow === 'verify' && data) {
|
|
101
|
+
return (
|
|
102
|
+
<div style={styles.container}>
|
|
103
|
+
<div style={styles.title}>Verify Setup</div>
|
|
104
|
+
<div style={styles.subtitle}>
|
|
105
|
+
Scan the QR code with your authenticator app, then enter the 6-digit code.
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{/* QR Code placeholder — replace with actual qrcode generation in your app */}
|
|
109
|
+
<div style={styles.qrPlaceholder}>
|
|
110
|
+
<div>
|
|
111
|
+
<div style={{ marginBottom: 8 }}>🔌</div>
|
|
112
|
+
QR Code
|
|
113
|
+
<br />
|
|
114
|
+
<span style={{ fontSize: 11 }}>Use a qrcode library to render:</span>
|
|
115
|
+
<br />
|
|
116
|
+
<span style={{ fontSize: 11, fontFamily: 'monospace' }}>
|
|
117
|
+
{data.qrCodeUri.substring(0, 60)}...
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div style={styles.secretBox}>
|
|
123
|
+
Or enter manually: <strong>{data.secret}</strong>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<input
|
|
127
|
+
style={styles.input}
|
|
128
|
+
type="text"
|
|
129
|
+
inputMode="numeric"
|
|
130
|
+
maxLength={6}
|
|
131
|
+
placeholder="000000"
|
|
132
|
+
value={code}
|
|
133
|
+
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
|
134
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleVerify(); }}
|
|
135
|
+
autoFocus
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
<button
|
|
139
|
+
style={loading || code.length !== 6 ? styles.buttonDisabled : styles.button}
|
|
140
|
+
onClick={handleVerify}
|
|
141
|
+
disabled={loading || code.length !== 6}
|
|
142
|
+
>
|
|
143
|
+
{loading ? 'Verifying...' : 'Confirm'}
|
|
144
|
+
</button>
|
|
145
|
+
|
|
146
|
+
{error && <div style={styles.error}>{error}</div>}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div style={styles.container}>
|
|
153
|
+
<div style={styles.title}>Set Up Two-Factor Authentication</div>
|
|
154
|
+
<div style={styles.subtitle}>
|
|
155
|
+
Add an extra layer of security to your account by enabling
|
|
156
|
+
time-based one-time passwords (TOTP).
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<button
|
|
160
|
+
style={loading ? styles.buttonDisabled : styles.button}
|
|
161
|
+
onClick={handleGenerate}
|
|
162
|
+
disabled={loading}
|
|
163
|
+
>
|
|
164
|
+
{loading ? 'Generating...' : 'Enable Authenticator App'}
|
|
165
|
+
</button>
|
|
166
|
+
|
|
167
|
+
{error && <div style={styles.error}>{error}</div>}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @ts-nocheck — vitest mock types
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { mfaPlugin } from '../index';
|
|
4
|
+
|
|
5
|
+
describe('plugin-mfa', () => {
|
|
6
|
+
describe('mfaPlugin', () => {
|
|
7
|
+
it('returns an object with name and version', () => {
|
|
8
|
+
const plugin = mfaPlugin();
|
|
9
|
+
expect(plugin.name).toBeDefined();
|
|
10
|
+
expect(typeof plugin.name).toBe('string');
|
|
11
|
+
expect(plugin.version).toBeDefined();
|
|
12
|
+
expect(typeof plugin.version).toBe('string');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('has install method', () => {
|
|
16
|
+
const plugin = mfaPlugin();
|
|
17
|
+
expect(plugin.install).toBeDefined();
|
|
18
|
+
expect(typeof plugin.install).toBe('function');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('install calls core registration', () => {
|
|
22
|
+
const plugin = mfaPlugin();
|
|
23
|
+
const mockCore: any = { use: () => {} };
|
|
24
|
+
expect(() => plugin.install(mockCore)).not.toThrow();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
package/src/index.ts
ADDED
package/src/mfa.ts
ADDED