@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.
@@ -0,0 +1,509 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var React = require('react');
5
+ var designSystem = require('@strapi/design-system');
6
+ var camelCase = require('lodash/camelCase');
7
+ var reactIntl = require('react-intl');
8
+ var reactRouterDom = require('react-router-dom');
9
+ var yup = require('yup');
10
+ var Form = require('../../../components/Form.js');
11
+ var Renderer = require('../../../components/FormInputs/Renderer.js');
12
+ var UnauthenticatedLogo = require('../../../components/UnauthenticatedLogo.js');
13
+ var UnauthenticatedLayout = require('../../../layouts/UnauthenticatedLayout.js');
14
+ var hooks = require('../../../core/store/hooks.js');
15
+ var Notifications = require('../../../features/Notifications.js');
16
+ var reducer = require('../../../reducer.js');
17
+ var auth = require('../../../services/auth.js');
18
+ var deviceId = require('../../../utils/deviceId.js');
19
+ var translatedErrors = require('../../../utils/translatedErrors.js');
20
+
21
+ function _interopNamespaceDefault(e) {
22
+ var n = Object.create(null);
23
+ if (e) {
24
+ Object.keys(e).forEach(function (k) {
25
+ if (k !== 'default') {
26
+ var d = Object.getOwnPropertyDescriptor(e, k);
27
+ Object.defineProperty(n, k, d.get ? d : {
28
+ enumerable: true,
29
+ get: function () { return e[k]; }
30
+ });
31
+ }
32
+ });
33
+ }
34
+ n.default = e;
35
+ return Object.freeze(n);
36
+ }
37
+
38
+ var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
39
+ var yup__namespace = /*#__PURE__*/_interopNamespaceDefault(yup);
40
+
41
+ const OTP_LENGTH = 6;
42
+ const OTP_DIGIT_INPUT_STYLE = {
43
+ width: 'min(3.75rem, calc((100vw - 7rem) / 6))',
44
+ minWidth: '2.7rem',
45
+ maxWidth: '3.75rem',
46
+ flex: '1 1 0',
47
+ height: 'min(4.5rem, calc((100vw - 7rem) / 5.4))',
48
+ borderRadius: '1rem',
49
+ borderStyle: 'solid',
50
+ borderWidth: '2px',
51
+ borderColor: 'var(--strapi-colors-neutral500)',
52
+ backgroundColor: 'var(--strapi-colors-neutral200)',
53
+ color: 'var(--strapi-colors-neutral800)',
54
+ fontSize: '1.85rem',
55
+ fontWeight: 700,
56
+ textAlign: 'center',
57
+ outline: 'none',
58
+ transition: 'all 160ms ease',
59
+ boxShadow: 'none'
60
+ };
61
+ const sanitizeOtp = (value = '')=>value.replace(/\D/g, '').slice(0, OTP_LENGTH);
62
+ const createOtpDigits = (value = '')=>Array.from({
63
+ length: OTP_LENGTH
64
+ }, (_, index)=>value[index] ?? '');
65
+ const LOGIN_SCHEMA = yup__namespace.object().shape({
66
+ email: yup__namespace.string().nullable().email({
67
+ id: translatedErrors.translatedErrors.email.id,
68
+ defaultMessage: 'Not a valid email'
69
+ }).required(translatedErrors.translatedErrors.required),
70
+ password: yup__namespace.string().required(translatedErrors.translatedErrors.required).nullable(),
71
+ rememberMe: yup__namespace.bool().nullable()
72
+ });
73
+ const OTP_SCHEMA = yup__namespace.object().shape({
74
+ code: yup__namespace.string().nullable().matches(/^\d{6}$/, {
75
+ message: 'OTP code must be a 6-digit number'
76
+ }).required(translatedErrors.translatedErrors.required)
77
+ });
78
+ const OtpField = ()=>{
79
+ const { values, errors, onChange, isSubmitting } = Form.useForm('OtpField', (state)=>state);
80
+ const inputRefs = React__namespace.useRef([]);
81
+ const codeValue = typeof values.code === 'string' ? values.code : '';
82
+ const digits = React__namespace.useMemo(()=>createOtpDigits(codeValue), [
83
+ codeValue
84
+ ]);
85
+ const [activeIndex, setActiveIndex] = React__namespace.useState(0);
86
+ const focusInput = React__namespace.useCallback((index)=>{
87
+ window.requestAnimationFrame(()=>{
88
+ const element = inputRefs.current[index];
89
+ if (element) {
90
+ element.focus();
91
+ element.select();
92
+ }
93
+ });
94
+ }, []);
95
+ React__namespace.useEffect(()=>{
96
+ if (!codeValue) {
97
+ focusInput(0);
98
+ }
99
+ }, [
100
+ codeValue,
101
+ focusInput
102
+ ]);
103
+ const commitDigits = React__namespace.useCallback((nextDigits, focusIndex)=>{
104
+ const nextCode = nextDigits.join('');
105
+ onChange('code', nextCode || null);
106
+ if (typeof focusIndex === 'number') {
107
+ focusInput(focusIndex);
108
+ }
109
+ }, [
110
+ focusInput,
111
+ onChange
112
+ ]);
113
+ const handleChange = (index)=>(event)=>{
114
+ const incomingValue = sanitizeOtp(event.target.value);
115
+ const nextDigits = [
116
+ ...digits
117
+ ];
118
+ if (!incomingValue) {
119
+ nextDigits[index] = '';
120
+ commitDigits(nextDigits, index);
121
+ return;
122
+ }
123
+ incomingValue.split('').forEach((digit, offset)=>{
124
+ const targetIndex = index + offset;
125
+ if (targetIndex < OTP_LENGTH) {
126
+ nextDigits[targetIndex] = digit;
127
+ }
128
+ });
129
+ const nextFocusIndex = Math.min(index + incomingValue.length, OTP_LENGTH - 1);
130
+ commitDigits(nextDigits, nextFocusIndex);
131
+ };
132
+ const handleKeyDown = (index)=>(event)=>{
133
+ if (event.key === 'Backspace') {
134
+ event.preventDefault();
135
+ const nextDigits = [
136
+ ...digits
137
+ ];
138
+ if (nextDigits[index]) {
139
+ nextDigits[index] = '';
140
+ commitDigits(nextDigits, index);
141
+ } else if (index > 0) {
142
+ nextDigits[index - 1] = '';
143
+ commitDigits(nextDigits, index - 1);
144
+ }
145
+ return;
146
+ }
147
+ if (event.key === 'ArrowLeft' && index > 0) {
148
+ event.preventDefault();
149
+ focusInput(index - 1);
150
+ return;
151
+ }
152
+ if (event.key === 'ArrowRight' && index < OTP_LENGTH - 1) {
153
+ event.preventDefault();
154
+ focusInput(index + 1);
155
+ }
156
+ };
157
+ const handlePaste = (event)=>{
158
+ const pastedValue = sanitizeOtp(event.clipboardData.getData('text'));
159
+ if (!pastedValue) {
160
+ return;
161
+ }
162
+ event.preventDefault();
163
+ const nextDigits = createOtpDigits();
164
+ pastedValue.split('').forEach((digit, index)=>{
165
+ nextDigits[index] = digit;
166
+ });
167
+ commitDigits(nextDigits, Math.min(pastedValue.length, OTP_LENGTH) - 1);
168
+ };
169
+ return /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Box, {
170
+ padding: 5,
171
+ style: {
172
+ borderRadius: '1.25rem',
173
+ background: 'var(--strapi-colors-neutral150)',
174
+ border: '1px solid var(--strapi-colors-neutral300)'
175
+ },
176
+ children: [
177
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Box, {
178
+ paddingBottom: 2,
179
+ children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.Flex, {
180
+ gap: 2,
181
+ justifyContent: "center",
182
+ wrap: "nowrap",
183
+ width: "100%",
184
+ style: {
185
+ maxWidth: '24rem',
186
+ marginInline: 'auto'
187
+ },
188
+ onPaste: handlePaste,
189
+ children: digits.map((digit, index)=>/*#__PURE__*/ jsxRuntime.jsx("input", {
190
+ "aria-invalid": errors.code ? 'true' : 'false',
191
+ "aria-label": `OTP digit ${index + 1}`,
192
+ autoComplete: index === 0 ? 'one-time-code' : 'off',
193
+ disabled: isSubmitting,
194
+ inputMode: "numeric",
195
+ maxLength: 6,
196
+ onChange: handleChange(index),
197
+ onFocus: ()=>setActiveIndex(index),
198
+ onKeyDown: handleKeyDown(index),
199
+ pattern: "[0-9]*",
200
+ ref: (element)=>{
201
+ inputRefs.current[index] = element;
202
+ },
203
+ style: {
204
+ ...OTP_DIGIT_INPUT_STYLE,
205
+ borderColor: errors.code ? 'var(--strapi-colors-danger600)' : activeIndex === index ? 'var(--strapi-colors-primary600)' : 'var(--strapi-colors-neutral500)',
206
+ backgroundColor: activeIndex === index ? 'var(--strapi-colors-neutral0)' : 'var(--strapi-colors-neutral200)',
207
+ boxShadow: errors.code ? '0 0 0 1px var(--strapi-colors-danger600)' : activeIndex === index ? '0 0 0 3px rgba(73, 69, 255, 0.18)' : 'inset 0 0 0 1px var(--strapi-colors-neutral600)'
208
+ },
209
+ type: "text",
210
+ value: digit
211
+ }, index))
212
+ })
213
+ }),
214
+ errors.code ? /*#__PURE__*/ jsxRuntime.jsx(designSystem.Box, {
215
+ paddingTop: 3,
216
+ children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.Typography, {
217
+ id: "otp-code-error",
218
+ variant: "pi",
219
+ textColor: "danger600",
220
+ children: errors.code
221
+ })
222
+ }) : null
223
+ ]
224
+ });
225
+ };
226
+ const Login = ({ children })=>{
227
+ const [apiError, setApiError] = React__namespace.useState();
228
+ const [otpStep, setOtpStep] = React__namespace.useState(null);
229
+ const { formatMessage } = reactIntl.useIntl();
230
+ const { search: searchString } = reactRouterDom.useLocation();
231
+ const query = React__namespace.useMemo(()=>new URLSearchParams(searchString), [
232
+ searchString
233
+ ]);
234
+ const navigate = reactRouterDom.useNavigate();
235
+ const dispatch = hooks.useTypedDispatch();
236
+ const { toggleNotification } = Notifications.useNotification();
237
+ const [adminLoginWithOtp, { isLoading: isLoggingIn }] = auth.useAdminLoginWithOtpMutation();
238
+ const [verifyAdminLoginOtp, { isLoading: isVerifyingOtp }] = auth.useVerifyAdminLoginOtpMutation();
239
+ const [resendAdminLoginOtp, { isLoading: isResendingOtp }] = auth.useResendAdminLoginOtpMutation();
240
+ React__namespace.useEffect(()=>{
241
+ document.title = 'Admin Dashboard';
242
+ }, []);
243
+ const handleLogin = async (body)=>{
244
+ setApiError(undefined);
245
+ const res = await adminLoginWithOtp({
246
+ ...body,
247
+ deviceId: deviceId.getOrCreateDeviceId()
248
+ });
249
+ if ('error' in res) {
250
+ const message = res.error.message ?? 'Something went wrong';
251
+ if (camelCase(message).toLowerCase() === 'usernotactive') {
252
+ navigate('/auth/oops');
253
+ return;
254
+ }
255
+ setApiError(message);
256
+ } else {
257
+ setOtpStep({
258
+ challengeId: res.data.challengeId,
259
+ expiresAt: res.data.expiresAt,
260
+ maskedEmail: res.data.maskedEmail,
261
+ rememberMe: body.rememberMe
262
+ });
263
+ }
264
+ };
265
+ const handleVerifyOtp = async ({ code })=>{
266
+ if (!otpStep) {
267
+ return;
268
+ }
269
+ setApiError(undefined);
270
+ const res = await verifyAdminLoginOtp({
271
+ challengeId: otpStep.challengeId,
272
+ code
273
+ });
274
+ if ('error' in res) {
275
+ setApiError(res.error.message ?? 'Something went wrong');
276
+ } else {
277
+ toggleNotification({
278
+ type: 'success',
279
+ title: formatMessage({
280
+ id: 'Auth.notification.authenticated.title',
281
+ defaultMessage: 'Successfully authenticated'
282
+ })
283
+ });
284
+ dispatch(reducer.login({
285
+ token: res.data.token,
286
+ persist: otpStep.rememberMe
287
+ }));
288
+ const redirectTo = query.get('redirectTo');
289
+ const redirectUrl = redirectTo ? decodeURIComponent(redirectTo) : '/';
290
+ navigate(redirectUrl);
291
+ }
292
+ };
293
+ const handleResendOtp = async ()=>{
294
+ if (!otpStep) {
295
+ return;
296
+ }
297
+ setApiError(undefined);
298
+ const res = await resendAdminLoginOtp({
299
+ challengeId: otpStep.challengeId
300
+ });
301
+ if ('error' in res) {
302
+ setApiError(res.error.message ?? 'Something went wrong');
303
+ } else {
304
+ setOtpStep({
305
+ ...otpStep,
306
+ expiresAt: res.data.expiresAt,
307
+ maskedEmail: res.data.maskedEmail
308
+ });
309
+ toggleNotification({
310
+ type: 'success',
311
+ title: formatMessage({
312
+ id: 'Auth.notification.otpResent.title',
313
+ defaultMessage: 'OTP resent'
314
+ }),
315
+ message: formatMessage({
316
+ id: 'Auth.notification.otpResent.message',
317
+ defaultMessage: `A new OTP has been sent to ${res.data.maskedEmail}.`
318
+ })
319
+ });
320
+ }
321
+ };
322
+ return /*#__PURE__*/ jsxRuntime.jsx(UnauthenticatedLayout.UnauthenticatedLayout, {
323
+ children: /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Main, {
324
+ children: [
325
+ /*#__PURE__*/ jsxRuntime.jsxs(UnauthenticatedLayout.LayoutContent, {
326
+ children: [
327
+ /*#__PURE__*/ jsxRuntime.jsxs(UnauthenticatedLayout.Column, {
328
+ children: [
329
+ /*#__PURE__*/ jsxRuntime.jsx(UnauthenticatedLogo.Logo, {}),
330
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Box, {
331
+ paddingTop: 6,
332
+ paddingBottom: 1,
333
+ children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.Typography, {
334
+ variant: "alpha",
335
+ tag: "h1",
336
+ textAlign: "center",
337
+ children: formatMessage({
338
+ id: otpStep ? 'Auth.form.otp.title' : 'Auth.form.welcome.title',
339
+ defaultMessage: otpStep ? 'Enter your OTP code' : 'Welcome!'
340
+ })
341
+ })
342
+ }),
343
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Box, {
344
+ paddingBottom: otpStep ? 5 : 7,
345
+ children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.Typography, {
346
+ variant: "epsilon",
347
+ textColor: "neutral600",
348
+ textAlign: "center",
349
+ display: "block",
350
+ children: formatMessage({
351
+ id: otpStep ? 'Auth.form.otp.subtitle' : 'Auth.form.welcome.subtitle',
352
+ defaultMessage: otpStep ? `We sent a 6-digit code to ${otpStep.maskedEmail}` : 'Log in to your Strapi account'
353
+ })
354
+ })
355
+ }),
356
+ otpStep ? /*#__PURE__*/ jsxRuntime.jsx(designSystem.Box, {
357
+ paddingBottom: 5,
358
+ children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.Typography, {
359
+ variant: "pi",
360
+ textColor: "neutral600",
361
+ textAlign: "center",
362
+ children: `OTP expires at ${new Date(otpStep.expiresAt).toLocaleTimeString()}`
363
+ })
364
+ }) : null,
365
+ apiError ? /*#__PURE__*/ jsxRuntime.jsx(designSystem.Box, {
366
+ paddingBottom: 4,
367
+ children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.Typography, {
368
+ id: "global-form-error",
369
+ role: "alert",
370
+ tabIndex: -1,
371
+ textColor: "danger600",
372
+ textAlign: "center",
373
+ children: apiError
374
+ })
375
+ }) : null
376
+ ]
377
+ }),
378
+ /*#__PURE__*/ jsxRuntime.jsx(Form.Form, {
379
+ method: "PUT",
380
+ initialValues: otpStep ? {
381
+ code: ''
382
+ } : {
383
+ email: '',
384
+ password: '',
385
+ rememberMe: false
386
+ },
387
+ onSubmit: (values)=>{
388
+ if (otpStep) {
389
+ handleVerifyOtp(values);
390
+ } else {
391
+ handleLogin(values);
392
+ }
393
+ },
394
+ validationSchema: otpStep ? OTP_SCHEMA : LOGIN_SCHEMA,
395
+ children: /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
396
+ direction: "column",
397
+ alignItems: "stretch",
398
+ gap: 6,
399
+ children: [
400
+ otpStep ? /*#__PURE__*/ jsxRuntime.jsx(OtpField, {}) : [
401
+ /*#__PURE__*/ jsxRuntime.jsx(Renderer.InputRenderer, {
402
+ label: formatMessage({
403
+ id: 'Auth.form.email.label',
404
+ defaultMessage: 'Email'
405
+ }),
406
+ name: "email",
407
+ placeholder: formatMessage({
408
+ id: 'Auth.form.email.placeholder',
409
+ defaultMessage: 'kai@doe.com'
410
+ }),
411
+ required: true,
412
+ type: "email"
413
+ }, "email"),
414
+ /*#__PURE__*/ jsxRuntime.jsx(Renderer.InputRenderer, {
415
+ label: formatMessage({
416
+ id: 'global.password',
417
+ defaultMessage: 'Password'
418
+ }),
419
+ name: "password",
420
+ required: true,
421
+ type: "password"
422
+ }, "password"),
423
+ /*#__PURE__*/ jsxRuntime.jsx(Renderer.InputRenderer, {
424
+ label: formatMessage({
425
+ id: 'Auth.form.rememberMe.label',
426
+ defaultMessage: 'Remember me'
427
+ }),
428
+ name: "rememberMe",
429
+ type: "checkbox"
430
+ }, "rememberMe")
431
+ ],
432
+ /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
433
+ direction: "column",
434
+ gap: 3,
435
+ children: [
436
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Button, {
437
+ fullWidth: true,
438
+ type: "submit",
439
+ disabled: otpStep ? isVerifyingOtp : isLoggingIn,
440
+ loading: otpStep ? isVerifyingOtp : isLoggingIn,
441
+ children: formatMessage({
442
+ id: otpStep ? 'Auth.form.button.verifyOtp' : 'Auth.form.button.login',
443
+ defaultMessage: otpStep ? isVerifyingOtp ? 'Verifying...' : 'Verify OTP' : isLoggingIn ? 'Login...' : 'Login'
444
+ })
445
+ }),
446
+ otpStep ? /*#__PURE__*/ jsxRuntime.jsxs(designSystem.Flex, {
447
+ gap: 2,
448
+ justifyContent: "space-between",
449
+ alignItems: "stretch",
450
+ children: [
451
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Button, {
452
+ fullWidth: true,
453
+ style: {
454
+ minWidth: '11rem'
455
+ },
456
+ type: "button",
457
+ variant: "secondary",
458
+ onClick: ()=>setOtpStep(null),
459
+ disabled: isResendingOtp,
460
+ children: formatMessage({
461
+ id: 'Auth.form.button.back',
462
+ defaultMessage: 'Back'
463
+ })
464
+ }),
465
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Button, {
466
+ fullWidth: true,
467
+ style: {
468
+ minWidth: '11rem'
469
+ },
470
+ type: "button",
471
+ variant: "tertiary",
472
+ onClick: handleResendOtp,
473
+ disabled: isResendingOtp,
474
+ children: formatMessage({
475
+ id: 'Auth.form.button.resendOtp',
476
+ defaultMessage: isResendingOtp ? 'Resending...' : 'Resend OTP'
477
+ })
478
+ })
479
+ ]
480
+ }) : null
481
+ ]
482
+ })
483
+ ]
484
+ })
485
+ }),
486
+ children
487
+ ]
488
+ }),
489
+ /*#__PURE__*/ jsxRuntime.jsx(designSystem.Flex, {
490
+ justifyContent: "center",
491
+ children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.Box, {
492
+ paddingTop: 4,
493
+ children: /*#__PURE__*/ jsxRuntime.jsx(designSystem.Link, {
494
+ isExternal: false,
495
+ tag: reactRouterDom.NavLink,
496
+ to: "/auth/forgot-password",
497
+ children: formatMessage({
498
+ id: 'Auth.link.forgot-password',
499
+ defaultMessage: 'Forgot your password?'
500
+ })
501
+ })
502
+ })
503
+ })
504
+ ]
505
+ })
506
+ });
507
+ };
508
+
509
+ exports.Login = Login;