@cosmicdrift/kumiko-bundled-features 0.157.0 → 0.157.1
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/package.json +6 -6
- package/src/auth-mfa/__tests__/errors-i18n-parity.test.ts +31 -0
- package/src/auth-mfa/constants.ts +3 -8
- package/src/auth-mfa/errors.ts +7 -7
- package/src/auth-mfa/web/i18n.ts +5 -0
- package/src/auth-mfa/web/index.ts +0 -1
- package/src/auth-mfa/web/mfa-disable-dialog.tsx +1 -2
- package/src/auth-mfa/web/mfa-enable-screen.tsx +5 -6
- package/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +1 -2
- package/src/tenant/handlers/invitations.query.ts +8 -3
- package/src/tenant/handlers/members.query.ts +20 -11
- package/src/auth-mfa/web/__tests__/mfa-error-keys.test.ts +0 -20
- package/src/auth-mfa/web/mfa-error-keys.ts +0 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.157.
|
|
3
|
+
"version": "0.157.1",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -116,11 +116,11 @@
|
|
|
116
116
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
117
117
|
},
|
|
118
118
|
"dependencies": {
|
|
119
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.157.
|
|
120
|
-
"@cosmicdrift/kumiko-framework": "0.157.
|
|
121
|
-
"@cosmicdrift/kumiko-headless": "0.157.
|
|
122
|
-
"@cosmicdrift/kumiko-renderer": "0.157.
|
|
123
|
-
"@cosmicdrift/kumiko-renderer-web": "0.157.
|
|
119
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.157.1",
|
|
120
|
+
"@cosmicdrift/kumiko-framework": "0.157.1",
|
|
121
|
+
"@cosmicdrift/kumiko-headless": "0.157.1",
|
|
122
|
+
"@cosmicdrift/kumiko-renderer": "0.157.1",
|
|
123
|
+
"@cosmicdrift/kumiko-renderer-web": "0.157.1",
|
|
124
124
|
"@mollie/api-client": "^4.5.0",
|
|
125
125
|
"imapflow": "^1.3.3",
|
|
126
126
|
"mailparser": "^3.9.8",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
invalidChallengeToken,
|
|
4
|
+
invalidRecoveryCode,
|
|
5
|
+
invalidSetupToken,
|
|
6
|
+
invalidTotpCode,
|
|
7
|
+
mfaAlreadyEnabled,
|
|
8
|
+
mfaNotEnabled,
|
|
9
|
+
tooManyAttempts,
|
|
10
|
+
} from "../errors";
|
|
11
|
+
import { defaultTranslations } from "../web/i18n";
|
|
12
|
+
|
|
13
|
+
const failures = {
|
|
14
|
+
mfaAlreadyEnabled: mfaAlreadyEnabled(),
|
|
15
|
+
mfaNotEnabled: mfaNotEnabled(),
|
|
16
|
+
invalidSetupToken: invalidSetupToken(),
|
|
17
|
+
invalidTotpCode: invalidTotpCode(),
|
|
18
|
+
invalidRecoveryCode: invalidRecoveryCode(),
|
|
19
|
+
invalidChallengeToken: invalidChallengeToken(),
|
|
20
|
+
tooManyAttempts: tooManyAttempts(30),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe("auth-mfa errors.ts i18nKey / client-bundle parity", () => {
|
|
24
|
+
for (const [name, failure] of Object.entries(failures)) {
|
|
25
|
+
test(`${name}'s i18nKey is registered in en/de translations`, () => {
|
|
26
|
+
const key = failure.error.i18nKey;
|
|
27
|
+
expect(defaultTranslations["en"]?.[key]).toBeDefined();
|
|
28
|
+
expect(defaultTranslations["de"]?.[key]).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
});
|
|
@@ -19,10 +19,9 @@ export const AuthMfaQueries = {
|
|
|
19
19
|
status: "auth-mfa:query:user-mfa:status",
|
|
20
20
|
} as const;
|
|
21
21
|
|
|
22
|
-
// Write-failure codes
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// two switches.
|
|
22
|
+
// Write-failure codes minted server-side into UnprocessableError (errors.ts).
|
|
23
|
+
// The client reads the i18n key straight off the write result
|
|
24
|
+
// (res.error.i18nKey) instead of re-deriving it from these codes.
|
|
26
25
|
export const AuthMfaErrorCodes = {
|
|
27
26
|
mfaAlreadyEnabled: "mfa_already_enabled",
|
|
28
27
|
mfaNotEnabled: "mfa_not_enabled",
|
|
@@ -31,10 +30,6 @@ export const AuthMfaErrorCodes = {
|
|
|
31
30
|
invalidRecoveryCode: "invalid_recovery_code",
|
|
32
31
|
invalidChallengeToken: "invalid_challenge_token",
|
|
33
32
|
tooManyAttempts: "too_many_attempts",
|
|
34
|
-
// Client-only: minted by mfa-enable-screen.tsx on a network/QR-render
|
|
35
|
-
// failure during enrollment, never by errors.ts server-side — kept here
|
|
36
|
-
// anyway so mfa-error-keys.ts's key-parity test covers it automatically.
|
|
37
|
-
setupFailed: "setup_failed",
|
|
38
33
|
} as const;
|
|
39
34
|
|
|
40
35
|
export const MFA_SETUP_TOKEN_TTL_MINUTES = 10;
|
package/src/auth-mfa/errors.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { AuthMfaErrorCodes as AuthMfaErrors } from "./constants";
|
|
|
10
10
|
export function mfaAlreadyEnabled(): WriteFailure {
|
|
11
11
|
return writeFailure(
|
|
12
12
|
new UnprocessableError(AuthMfaErrorCodes.mfaAlreadyEnabled, {
|
|
13
|
-
i18nKey: "
|
|
13
|
+
i18nKey: "auth.mfa.errors.mfaAlreadyEnabled",
|
|
14
14
|
}),
|
|
15
15
|
);
|
|
16
16
|
}
|
|
@@ -18,7 +18,7 @@ export function mfaAlreadyEnabled(): WriteFailure {
|
|
|
18
18
|
export function mfaNotEnabled(): WriteFailure {
|
|
19
19
|
return writeFailure(
|
|
20
20
|
new UnprocessableError(AuthMfaErrorCodes.mfaNotEnabled, {
|
|
21
|
-
i18nKey: "
|
|
21
|
+
i18nKey: "auth.mfa.errors.mfaNotEnabled",
|
|
22
22
|
}),
|
|
23
23
|
);
|
|
24
24
|
}
|
|
@@ -26,7 +26,7 @@ export function mfaNotEnabled(): WriteFailure {
|
|
|
26
26
|
export function invalidSetupToken(): WriteFailure {
|
|
27
27
|
return writeFailure(
|
|
28
28
|
new UnprocessableError(AuthMfaErrorCodes.invalidSetupToken, {
|
|
29
|
-
i18nKey: "
|
|
29
|
+
i18nKey: "auth.mfa.errors.invalidSetupToken",
|
|
30
30
|
}),
|
|
31
31
|
);
|
|
32
32
|
}
|
|
@@ -34,7 +34,7 @@ export function invalidSetupToken(): WriteFailure {
|
|
|
34
34
|
export function invalidTotpCode(): WriteFailure {
|
|
35
35
|
return writeFailure(
|
|
36
36
|
new UnprocessableError(AuthMfaErrorCodes.invalidTotpCode, {
|
|
37
|
-
i18nKey: "
|
|
37
|
+
i18nKey: "auth.mfa.errors.invalidTotpCode",
|
|
38
38
|
}),
|
|
39
39
|
);
|
|
40
40
|
}
|
|
@@ -42,7 +42,7 @@ export function invalidTotpCode(): WriteFailure {
|
|
|
42
42
|
export function invalidRecoveryCode(): WriteFailure {
|
|
43
43
|
return writeFailure(
|
|
44
44
|
new UnprocessableError(AuthMfaErrorCodes.invalidRecoveryCode, {
|
|
45
|
-
i18nKey: "
|
|
45
|
+
i18nKey: "auth.mfa.errors.invalidRecoveryCode",
|
|
46
46
|
}),
|
|
47
47
|
);
|
|
48
48
|
}
|
|
@@ -50,7 +50,7 @@ export function invalidRecoveryCode(): WriteFailure {
|
|
|
50
50
|
export function invalidChallengeToken(): WriteFailure {
|
|
51
51
|
return writeFailure(
|
|
52
52
|
new UnprocessableError(AuthMfaErrorCodes.invalidChallengeToken, {
|
|
53
|
-
i18nKey: "
|
|
53
|
+
i18nKey: "auth.mfa.errors.invalidChallengeToken",
|
|
54
54
|
}),
|
|
55
55
|
);
|
|
56
56
|
}
|
|
@@ -59,7 +59,7 @@ export function invalidChallengeToken(): WriteFailure {
|
|
|
59
59
|
export function tooManyAttempts(retryAfterSeconds: number): WriteFailure {
|
|
60
60
|
return writeFailure(
|
|
61
61
|
new UnprocessableError(AuthMfaErrorCodes.tooManyAttempts, {
|
|
62
|
-
i18nKey: "
|
|
62
|
+
i18nKey: "auth.mfa.errors.tooManyAttempts",
|
|
63
63
|
details: { retryAfterSeconds },
|
|
64
64
|
}),
|
|
65
65
|
);
|
package/src/auth-mfa/web/i18n.ts
CHANGED
|
@@ -15,6 +15,9 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
15
15
|
"auth.mfa.verify.submitting": "…",
|
|
16
16
|
"auth.mfa.verify.backToLogin": "Zurück zum Login",
|
|
17
17
|
"auth.mfa.errors.invalidCode": "Ungültiger Code. Bitte erneut versuchen.",
|
|
18
|
+
"auth.mfa.errors.invalidTotpCode": "Ungültiger Code. Bitte erneut versuchen.",
|
|
19
|
+
"auth.mfa.errors.invalidChallengeToken":
|
|
20
|
+
"Die Anmeldung ist abgelaufen. Bitte erneut einloggen.",
|
|
18
21
|
"auth.mfa.errors.challengeExpired": "Die Anmeldung ist abgelaufen. Bitte erneut einloggen.",
|
|
19
22
|
"auth.mfa.errors.tooManyAttempts": "Zu viele Fehlversuche. Bitte erneut einloggen.",
|
|
20
23
|
"auth.mfa.errors.verifyFailed": "Bestätigung fehlgeschlagen.",
|
|
@@ -66,6 +69,8 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
66
69
|
"auth.mfa.verify.submitting": "…",
|
|
67
70
|
"auth.mfa.verify.backToLogin": "Back to login",
|
|
68
71
|
"auth.mfa.errors.invalidCode": "Invalid code. Please try again.",
|
|
72
|
+
"auth.mfa.errors.invalidTotpCode": "Invalid code. Please try again.",
|
|
73
|
+
"auth.mfa.errors.invalidChallengeToken": "Your sign-in has expired. Please sign in again.",
|
|
69
74
|
"auth.mfa.errors.challengeExpired": "Your sign-in has expired. Please sign in again.",
|
|
70
75
|
"auth.mfa.errors.tooManyAttempts": "Too many failed attempts. Please sign in again.",
|
|
71
76
|
"auth.mfa.errors.verifyFailed": "Verification failed.",
|
|
@@ -13,7 +13,6 @@ export type { MfaDisableDialogProps } from "./mfa-disable-dialog";
|
|
|
13
13
|
export { MfaDisableDialog } from "./mfa-disable-dialog";
|
|
14
14
|
export type { MfaEnableScreenProps } from "./mfa-enable-screen";
|
|
15
15
|
export { MfaEnableScreen } from "./mfa-enable-screen";
|
|
16
|
-
export { mfaManageErrorKey } from "./mfa-error-keys";
|
|
17
16
|
export type { MfaRecoveryCodesRevealProps } from "./mfa-recovery-codes-reveal";
|
|
18
17
|
export { MfaRecoveryCodesReveal } from "./mfa-recovery-codes-reveal";
|
|
19
18
|
export type { MfaRegenerateRecoveryDialogProps } from "./mfa-regenerate-recovery-dialog";
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
14
14
|
import { type ReactNode, useState } from "react";
|
|
15
15
|
import { AuthMfaHandlers } from "../constants";
|
|
16
|
-
import { mfaManageErrorKey } from "./mfa-error-keys";
|
|
17
16
|
|
|
18
17
|
export type MfaDisableDialogProps = {
|
|
19
18
|
readonly open: boolean;
|
|
@@ -37,7 +36,7 @@ export function MfaDisableDialog({
|
|
|
37
36
|
const res = await dispatcher.write<{ disabled: boolean }>(AuthMfaHandlers.disable, { code });
|
|
38
37
|
setCode("");
|
|
39
38
|
if (!res.isSuccess) {
|
|
40
|
-
onError(
|
|
39
|
+
onError(res.error.i18nKey);
|
|
41
40
|
return;
|
|
42
41
|
}
|
|
43
42
|
onDisabled();
|
|
@@ -21,7 +21,6 @@ import { type ReactNode, useState } from "react";
|
|
|
21
21
|
// kumiko-lint-ignore cross-feature-import client-only hook, the feature's server barrel has no web/ re-export
|
|
22
22
|
import { useSession } from "../../auth-email-password/web";
|
|
23
23
|
import { AuthMfaHandlers } from "../constants";
|
|
24
|
-
import { mfaManageErrorKey } from "./mfa-error-keys";
|
|
25
24
|
|
|
26
25
|
type EnableStartResponse = {
|
|
27
26
|
readonly setupToken: string;
|
|
@@ -77,7 +76,7 @@ export function MfaEnableScreen({
|
|
|
77
76
|
accountLabel: session.user?.email ?? "",
|
|
78
77
|
});
|
|
79
78
|
if (!res.isSuccess) {
|
|
80
|
-
setError(res.error.
|
|
79
|
+
setError(res.error.i18nKey);
|
|
81
80
|
return;
|
|
82
81
|
}
|
|
83
82
|
// errorCorrectionLevel "H" (~30% redundancy) — more resilient to
|
|
@@ -96,7 +95,7 @@ export function MfaEnableScreen({
|
|
|
96
95
|
setAcknowledged(false);
|
|
97
96
|
setCode("");
|
|
98
97
|
} catch {
|
|
99
|
-
setError("
|
|
98
|
+
setError("auth.mfa.errors.setupFailed");
|
|
100
99
|
} finally {
|
|
101
100
|
setBusy(false);
|
|
102
101
|
}
|
|
@@ -112,14 +111,14 @@ export function MfaEnableScreen({
|
|
|
112
111
|
code,
|
|
113
112
|
});
|
|
114
113
|
if (!res.isSuccess) {
|
|
115
|
-
setError(res.error.
|
|
114
|
+
setError(res.error.i18nKey);
|
|
116
115
|
return;
|
|
117
116
|
}
|
|
118
117
|
setEnabled(true);
|
|
119
118
|
setSetup(null);
|
|
120
119
|
onEnabled?.();
|
|
121
120
|
} catch {
|
|
122
|
-
setError("
|
|
121
|
+
setError("auth.mfa.errors.setupFailed");
|
|
123
122
|
} finally {
|
|
124
123
|
setBusy(false);
|
|
125
124
|
}
|
|
@@ -130,7 +129,7 @@ export function MfaEnableScreen({
|
|
|
130
129
|
<Heading>{t("auth.mfa.enable.title")}</Heading>
|
|
131
130
|
|
|
132
131
|
{enabled && <Banner variant="info">{t("auth.mfa.enable.success")}</Banner>}
|
|
133
|
-
{error !== null && <Banner variant="error">{t(
|
|
132
|
+
{error !== null && <Banner variant="error">{t(error)}</Banner>}
|
|
134
133
|
|
|
135
134
|
{!setup && !enabled && (
|
|
136
135
|
<Section
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
11
11
|
import { type ReactNode, useState } from "react";
|
|
12
12
|
import { AuthMfaHandlers } from "../constants";
|
|
13
|
-
import { mfaManageErrorKey } from "./mfa-error-keys";
|
|
14
13
|
|
|
15
14
|
export type MfaRegenerateRecoveryDialogProps = {
|
|
16
15
|
readonly open: boolean;
|
|
@@ -37,7 +36,7 @@ export function MfaRegenerateRecoveryDialog({
|
|
|
37
36
|
);
|
|
38
37
|
setCode("");
|
|
39
38
|
if (!res.isSuccess) {
|
|
40
|
-
onError(
|
|
39
|
+
onError(res.error.i18nKey);
|
|
41
40
|
return;
|
|
42
41
|
}
|
|
43
42
|
onRegenerated(res.data.recoveryCodes);
|
|
@@ -25,9 +25,14 @@ export const invitationsQuery = defineQueryHandler({
|
|
|
25
25
|
return Promise.all(
|
|
26
26
|
(rows ?? []).map(async (row) => {
|
|
27
27
|
const email = row["email"];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
:
|
|
28
|
+
const invitedBy = row["invitedBy"];
|
|
29
|
+
const decryptedEmail =
|
|
30
|
+
typeof email === "string" ? await decryptStoredPii(email, "tenant:invitations") : email;
|
|
31
|
+
const decryptedInvitedBy =
|
|
32
|
+
typeof invitedBy === "string"
|
|
33
|
+
? await decryptStoredPii(invitedBy, "tenant:invitations")
|
|
34
|
+
: invitedBy;
|
|
35
|
+
return { ...row, email: decryptedEmail, invitedBy: decryptedInvitedBy };
|
|
31
36
|
}),
|
|
32
37
|
);
|
|
33
38
|
},
|
|
@@ -2,6 +2,7 @@ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
|
2
2
|
import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { decryptStoredPii } from "../../shared";
|
|
5
6
|
import { userTable } from "../../user";
|
|
6
7
|
import { tenantMembershipsTable } from "../membership-table";
|
|
7
8
|
|
|
@@ -21,16 +22,24 @@ export const membersQuery = defineQueryHandler({
|
|
|
21
22
|
userIds.length > 0 ? await selectMany<UserRow>(ctx.db, userTable, { id: userIds }) : [];
|
|
22
23
|
const userById = new Map(users.map((u) => [String(u.id), u]));
|
|
23
24
|
|
|
24
|
-
return
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
displayName
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
return Promise.all(
|
|
26
|
+
rows.map(async (row) => {
|
|
27
|
+
const user = userById.get(String(row["userId"]));
|
|
28
|
+
const email =
|
|
29
|
+
typeof user?.email === "string"
|
|
30
|
+
? await decryptStoredPii(user.email, "tenant:members")
|
|
31
|
+
: null;
|
|
32
|
+
const displayName =
|
|
33
|
+
typeof user?.displayName === "string"
|
|
34
|
+
? await decryptStoredPii(user.displayName, "tenant:members")
|
|
35
|
+
: null;
|
|
36
|
+
return {
|
|
37
|
+
...row,
|
|
38
|
+
email,
|
|
39
|
+
displayName,
|
|
40
|
+
roles: parseRoles(row["roles"]),
|
|
41
|
+
};
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
35
44
|
},
|
|
36
45
|
});
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { AuthMfaErrorCodes } from "../../constants";
|
|
3
|
-
import { defaultTranslations } from "../i18n";
|
|
4
|
-
import { mfaManageErrorKey } from "../mfa-error-keys";
|
|
5
|
-
|
|
6
|
-
describe("mfaManageErrorKey", () => {
|
|
7
|
-
for (const code of Object.values(AuthMfaErrorCodes)) {
|
|
8
|
-
test(`${code} maps to a key registered in en/de translations`, () => {
|
|
9
|
-
const key = mfaManageErrorKey(code);
|
|
10
|
-
expect(defaultTranslations["en"]?.[key]).toBeDefined();
|
|
11
|
-
expect(defaultTranslations["de"]?.[key]).toBeDefined();
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
test("unknown code falls back to the generic verifyFailed key", () => {
|
|
16
|
-
const key = mfaManageErrorKey("some_future_server_code");
|
|
17
|
-
expect(key).toBe("auth.mfa.errors.verifyFailed");
|
|
18
|
-
expect(defaultTranslations["en"]?.[key]).toBeDefined();
|
|
19
|
-
});
|
|
20
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// @runtime client
|
|
2
|
-
// Shared error-code → i18n-key mapping for the account-management write
|
|
3
|
-
// handlers (enable-confirm/disable/regenerate-recovery) — distinct from
|
|
4
|
-
// mfa-verify-screen.tsx's own mapper, which covers the login-challenge
|
|
5
|
-
// error surface (challenge_expired/rate_limited) that these handlers never
|
|
6
|
-
// return. Centralized so enable/disable/regenerate stay in sync instead of
|
|
7
|
-
// each re-deriving the key ad hoc (enable-screen used to template-string
|
|
8
|
-
// the raw snake_case code straight into the key, which never matched the
|
|
9
|
-
// camelCase keys actually registered below).
|
|
10
|
-
import { AuthMfaErrorCodes } from "../constants";
|
|
11
|
-
|
|
12
|
-
export function mfaManageErrorKey(code: string): string {
|
|
13
|
-
switch (code) {
|
|
14
|
-
case AuthMfaErrorCodes.invalidTotpCode:
|
|
15
|
-
return "auth.mfa.errors.invalidCode";
|
|
16
|
-
case AuthMfaErrorCodes.invalidRecoveryCode:
|
|
17
|
-
return "auth.mfa.errors.invalidRecoveryCode";
|
|
18
|
-
case AuthMfaErrorCodes.mfaAlreadyEnabled:
|
|
19
|
-
return "auth.mfa.errors.mfaAlreadyEnabled";
|
|
20
|
-
case AuthMfaErrorCodes.mfaNotEnabled:
|
|
21
|
-
return "auth.mfa.errors.mfaNotEnabled";
|
|
22
|
-
case AuthMfaErrorCodes.invalidSetupToken:
|
|
23
|
-
return "auth.mfa.errors.invalidSetupToken";
|
|
24
|
-
case AuthMfaErrorCodes.setupFailed:
|
|
25
|
-
return "auth.mfa.errors.setupFailed";
|
|
26
|
-
default:
|
|
27
|
-
return "auth.mfa.errors.verifyFailed";
|
|
28
|
-
}
|
|
29
|
-
}
|