@stackframe/stack 2.8.2 → 2.8.5
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/CHANGELOG.md +29 -0
- package/dist/components/api-key-dialogs.js +184 -0
- package/dist/components/api-key-dialogs.js.map +1 -0
- package/dist/components/api-key-table.js +149 -0
- package/dist/components/api-key-table.js.map +1 -0
- package/dist/components-page/account-settings/active-sessions/active-sessions-page.js +151 -0
- package/dist/components-page/account-settings/active-sessions/active-sessions-page.js.map +1 -0
- package/dist/components-page/account-settings/api-keys/api-keys-page.js +70 -0
- package/dist/components-page/account-settings/api-keys/api-keys-page.js.map +1 -0
- package/dist/components-page/account-settings/editable-text.js +75 -0
- package/dist/components-page/account-settings/editable-text.js.map +1 -0
- package/dist/components-page/account-settings/email-and-auth/email-and-auth-page.js +46 -0
- package/dist/components-page/account-settings/email-and-auth/email-and-auth-page.js.map +1 -0
- package/dist/components-page/account-settings/email-and-auth/emails-section.js +188 -0
- package/dist/components-page/account-settings/email-and-auth/emails-section.js.map +1 -0
- package/dist/components-page/account-settings/email-and-auth/mfa-section.js +146 -0
- package/dist/components-page/account-settings/email-and-auth/mfa-section.js.map +1 -0
- package/dist/components-page/account-settings/email-and-auth/otp-section.js +89 -0
- package/dist/components-page/account-settings/email-and-auth/otp-section.js.map +1 -0
- package/dist/components-page/account-settings/email-and-auth/passkey-section.js +92 -0
- package/dist/components-page/account-settings/email-and-auth/passkey-section.js.map +1 -0
- package/dist/components-page/account-settings/email-and-auth/password-section.js +181 -0
- package/dist/components-page/account-settings/email-and-auth/password-section.js.map +1 -0
- package/dist/components-page/account-settings/page-layout.js +34 -0
- package/dist/components-page/account-settings/page-layout.js.map +1 -0
- package/dist/components-page/account-settings/profile-page/profile-page.js +75 -0
- package/dist/components-page/account-settings/profile-page/profile-page.js.map +1 -0
- package/dist/components-page/account-settings/section.js +44 -0
- package/dist/components-page/account-settings/section.js.map +1 -0
- package/dist/components-page/account-settings/settings/delete-account-section.js +87 -0
- package/dist/components-page/account-settings/settings/delete-account-section.js.map +1 -0
- package/dist/components-page/account-settings/settings/settings-page.js +40 -0
- package/dist/components-page/account-settings/settings/settings-page.js.map +1 -0
- package/dist/components-page/account-settings/settings/sign-out-section.js +54 -0
- package/dist/components-page/account-settings/settings/sign-out-section.js.map +1 -0
- package/dist/components-page/account-settings/teams/leave-team-section.js +79 -0
- package/dist/components-page/account-settings/teams/leave-team-section.js.map +1 -0
- package/dist/components-page/account-settings/teams/team-api-keys-section.js +89 -0
- package/dist/components-page/account-settings/teams/team-api-keys-section.js.map +1 -0
- package/dist/components-page/account-settings/teams/team-creation-page.js +92 -0
- package/dist/components-page/account-settings/teams/team-creation-page.js.map +1 -0
- package/dist/components-page/account-settings/teams/team-display-name-section.js +57 -0
- package/dist/components-page/account-settings/teams/team-display-name-section.js.map +1 -0
- package/dist/components-page/account-settings/teams/team-member-invitation-section.js +131 -0
- package/dist/components-page/account-settings/teams/team-member-invitation-section.js.map +1 -0
- package/dist/components-page/account-settings/teams/team-member-list-section.js +61 -0
- package/dist/components-page/account-settings/teams/team-member-list-section.js.map +1 -0
- package/dist/components-page/account-settings/teams/team-page.js +50 -0
- package/dist/components-page/account-settings/teams/team-page.js.map +1 -0
- package/dist/components-page/account-settings/teams/team-profile-image-section.js +59 -0
- package/dist/components-page/account-settings/teams/team-profile-image-section.js.map +1 -0
- package/dist/components-page/account-settings/teams/team-profile-user-section.js +56 -0
- package/dist/components-page/account-settings/teams/team-profile-user-section.js.map +1 -0
- package/dist/components-page/account-settings.js +34 -1072
- package/dist/components-page/account-settings.js.map +1 -1
- package/dist/components-page/section.js +6 -0
- package/dist/components-page/section.js.map +1 -0
- package/dist/esm/components/api-key-dialogs.js +157 -0
- package/dist/esm/components/api-key-dialogs.js.map +1 -0
- package/dist/esm/components/api-key-table.js +125 -0
- package/dist/esm/components/api-key-table.js.map +1 -0
- package/dist/esm/components-page/account-settings/active-sessions/active-sessions-page.js +126 -0
- package/dist/esm/components-page/account-settings/active-sessions/active-sessions-page.js.map +1 -0
- package/dist/esm/components-page/account-settings/api-keys/api-keys-page.js +45 -0
- package/dist/esm/components-page/account-settings/api-keys/api-keys-page.js.map +1 -0
- package/dist/esm/components-page/account-settings/editable-text.js +50 -0
- package/dist/esm/components-page/account-settings/editable-text.js.map +1 -0
- package/dist/esm/components-page/account-settings/email-and-auth/email-and-auth-page.js +21 -0
- package/dist/esm/components-page/account-settings/email-and-auth/email-and-auth-page.js.map +1 -0
- package/dist/esm/components-page/account-settings/email-and-auth/emails-section.js +163 -0
- package/dist/esm/components-page/account-settings/email-and-auth/emails-section.js.map +1 -0
- package/dist/esm/components-page/account-settings/email-and-auth/mfa-section.js +111 -0
- package/dist/esm/components-page/account-settings/email-and-auth/mfa-section.js.map +1 -0
- package/dist/esm/components-page/account-settings/email-and-auth/otp-section.js +64 -0
- package/dist/esm/components-page/account-settings/email-and-auth/otp-section.js.map +1 -0
- package/dist/esm/components-page/account-settings/email-and-auth/passkey-section.js +67 -0
- package/dist/esm/components-page/account-settings/email-and-auth/passkey-section.js.map +1 -0
- package/dist/esm/components-page/account-settings/email-and-auth/password-section.js +146 -0
- package/dist/esm/components-page/account-settings/email-and-auth/password-section.js.map +1 -0
- package/dist/esm/components-page/account-settings/page-layout.js +9 -0
- package/dist/esm/components-page/account-settings/page-layout.js.map +1 -0
- package/dist/esm/components-page/account-settings/profile-page/profile-page.js +50 -0
- package/dist/esm/components-page/account-settings/profile-page/profile-page.js.map +1 -0
- package/dist/esm/components-page/account-settings/section.js +19 -0
- package/dist/esm/components-page/account-settings/section.js.map +1 -0
- package/dist/esm/components-page/account-settings/settings/delete-account-section.js +62 -0
- package/dist/esm/components-page/account-settings/settings/delete-account-section.js.map +1 -0
- package/dist/esm/components-page/account-settings/settings/settings-page.js +15 -0
- package/dist/esm/components-page/account-settings/settings/settings-page.js.map +1 -0
- package/dist/esm/components-page/account-settings/settings/sign-out-section.js +29 -0
- package/dist/esm/components-page/account-settings/settings/sign-out-section.js.map +1 -0
- package/dist/esm/components-page/account-settings/teams/leave-team-section.js +54 -0
- package/dist/esm/components-page/account-settings/teams/leave-team-section.js.map +1 -0
- package/dist/esm/components-page/account-settings/teams/team-api-keys-section.js +64 -0
- package/dist/esm/components-page/account-settings/teams/team-api-keys-section.js.map +1 -0
- package/dist/esm/components-page/account-settings/teams/team-creation-page.js +67 -0
- package/dist/esm/components-page/account-settings/teams/team-creation-page.js.map +1 -0
- package/dist/esm/components-page/account-settings/teams/team-display-name-section.js +32 -0
- package/dist/esm/components-page/account-settings/teams/team-display-name-section.js.map +1 -0
- package/dist/esm/components-page/account-settings/teams/team-member-invitation-section.js +106 -0
- package/dist/esm/components-page/account-settings/teams/team-member-invitation-section.js.map +1 -0
- package/dist/esm/components-page/account-settings/teams/team-member-list-section.js +36 -0
- package/dist/esm/components-page/account-settings/teams/team-member-list-section.js.map +1 -0
- package/dist/esm/components-page/account-settings/teams/team-page.js +25 -0
- package/dist/esm/components-page/account-settings/teams/team-page.js.map +1 -0
- package/dist/esm/components-page/account-settings/teams/team-profile-image-section.js +34 -0
- package/dist/esm/components-page/account-settings/teams/team-profile-image-section.js.map +1 -0
- package/dist/esm/components-page/account-settings/teams/team-profile-user-section.js +31 -0
- package/dist/esm/components-page/account-settings/teams/team-profile-user-section.js.map +1 -0
- package/dist/esm/components-page/account-settings.js +34 -1061
- package/dist/esm/components-page/account-settings.js.map +1 -1
- package/dist/esm/components-page/section.js +4 -0
- package/dist/esm/components-page/section.js.map +1 -0
- package/dist/esm/lib/stack-app/api-keys/index.js +14 -6
- package/dist/esm/lib/stack-app/api-keys/index.js.map +1 -1
- package/dist/esm/lib/stack-app/apps/implementations/admin-app-impl.js +26 -23
- package/dist/esm/lib/stack-app/apps/implementations/admin-app-impl.js.map +1 -1
- package/dist/esm/lib/stack-app/apps/implementations/client-app-impl.js +169 -0
- package/dist/esm/lib/stack-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/esm/lib/stack-app/apps/implementations/common.js +1 -1
- package/dist/esm/lib/stack-app/apps/implementations/common.js.map +1 -1
- package/dist/esm/lib/stack-app/apps/implementations/server-app-impl.js +148 -8
- package/dist/esm/lib/stack-app/apps/implementations/server-app-impl.js.map +1 -1
- package/dist/esm/lib/stack-app/apps/interfaces/admin-app.js.map +1 -1
- package/dist/esm/lib/stack-app/apps/interfaces/client-app.js.map +1 -1
- package/dist/esm/lib/stack-app/apps/interfaces/server-app.js.map +1 -1
- package/dist/esm/lib/stack-app/index.js.map +1 -1
- package/dist/esm/lib/stack-app/internal-api-keys/index.js +14 -0
- package/dist/esm/lib/stack-app/internal-api-keys/index.js.map +1 -0
- package/dist/esm/lib/stack-app/projects/index.js +3 -1
- package/dist/esm/lib/stack-app/projects/index.js.map +1 -1
- package/dist/esm/lib/stack-app/teams/index.js.map +1 -1
- package/dist/esm/lib/stack-app/users/index.js.map +1 -1
- package/dist/index.d.mts +103 -35
- package/dist/index.d.ts +103 -35
- package/dist/lib/stack-app/api-keys/index.js +16 -7
- package/dist/lib/stack-app/api-keys/index.js.map +1 -1
- package/dist/lib/stack-app/apps/implementations/admin-app-impl.js +25 -22
- package/dist/lib/stack-app/apps/implementations/admin-app-impl.js.map +1 -1
- package/dist/lib/stack-app/apps/implementations/client-app-impl.js +169 -0
- package/dist/lib/stack-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/lib/stack-app/apps/implementations/common.js +1 -1
- package/dist/lib/stack-app/apps/implementations/common.js.map +1 -1
- package/dist/lib/stack-app/apps/implementations/server-app-impl.js +148 -8
- package/dist/lib/stack-app/apps/implementations/server-app-impl.js.map +1 -1
- package/dist/lib/stack-app/apps/interfaces/admin-app.js.map +1 -1
- package/dist/lib/stack-app/apps/interfaces/client-app.js.map +1 -1
- package/dist/lib/stack-app/apps/interfaces/server-app.js.map +1 -1
- package/dist/lib/stack-app/index.js.map +1 -1
- package/dist/lib/stack-app/internal-api-keys/index.js +39 -0
- package/dist/lib/stack-app/internal-api-keys/index.js.map +1 -0
- package/dist/lib/stack-app/project-configs/index.js.map +1 -1
- package/dist/lib/stack-app/projects/index.js +3 -1
- package/dist/lib/stack-app/projects/index.js.map +1 -1
- package/dist/lib/stack-app/teams/index.js.map +1 -1
- package/dist/lib/stack-app/users/index.js.map +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// src/components-page/account-settings/active-sessions/active-sessions-page.tsx
|
|
2
|
+
import { fromNow } from "@stackframe/stack-shared/dist/utils/dates";
|
|
3
|
+
import { captureError } from "@stackframe/stack-shared/dist/utils/errors";
|
|
4
|
+
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
|
|
5
|
+
import { ActionCell, Badge, Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Typography } from "@stackframe/stack-ui";
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { useUser } from "../../../lib/hooks";
|
|
8
|
+
import { useTranslation } from "../../../lib/translations";
|
|
9
|
+
import { PageLayout } from "../page-layout";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
function ActiveSessionsPage() {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const user = useUser({ or: "throw" });
|
|
14
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
15
|
+
const [isRevokingAll, setIsRevokingAll] = useState(false);
|
|
16
|
+
const [sessions, setSessions] = useState([]);
|
|
17
|
+
const [showConfirmRevokeAll, setShowConfirmRevokeAll] = useState(false);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
runAsynchronously(async () => {
|
|
20
|
+
setIsLoading(true);
|
|
21
|
+
const sessionsData = await user.getActiveSessions();
|
|
22
|
+
const enhancedSessions = sessionsData;
|
|
23
|
+
setSessions(enhancedSessions);
|
|
24
|
+
setIsLoading(false);
|
|
25
|
+
});
|
|
26
|
+
}, [user]);
|
|
27
|
+
const handleRevokeSession = async (sessionId) => {
|
|
28
|
+
try {
|
|
29
|
+
await user.revokeSession(sessionId);
|
|
30
|
+
setSessions((prev) => prev.filter((session) => session.id !== sessionId));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
captureError("Failed to revoke session", { sessionId, error });
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const handleRevokeAllSessions = async () => {
|
|
37
|
+
setIsRevokingAll(true);
|
|
38
|
+
try {
|
|
39
|
+
const deletionPromises = sessions.filter((session) => !session.isCurrentSession).map((session) => user.revokeSession(session.id));
|
|
40
|
+
await Promise.all(deletionPromises);
|
|
41
|
+
setSessions((prevSessions) => prevSessions.filter((session) => session.isCurrentSession));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
captureError("Failed to revoke all sessions", { error, sessionIds: sessions.map((session) => session.id) });
|
|
44
|
+
throw error;
|
|
45
|
+
} finally {
|
|
46
|
+
setIsRevokingAll(false);
|
|
47
|
+
setShowConfirmRevokeAll(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
return /* @__PURE__ */ jsx(PageLayout, { children: /* @__PURE__ */ jsxs("div", { children: [
|
|
51
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center mb-2", children: [
|
|
52
|
+
/* @__PURE__ */ jsx(Typography, { className: "font-medium", children: t("Active Sessions") }),
|
|
53
|
+
sessions.filter((s) => !s.isCurrentSession).length > 0 && !isLoading && (showConfirmRevokeAll ? /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
54
|
+
/* @__PURE__ */ jsx(
|
|
55
|
+
Button,
|
|
56
|
+
{
|
|
57
|
+
variant: "destructive",
|
|
58
|
+
size: "sm",
|
|
59
|
+
loading: isRevokingAll,
|
|
60
|
+
onClick: handleRevokeAllSessions,
|
|
61
|
+
children: t("Confirm")
|
|
62
|
+
}
|
|
63
|
+
),
|
|
64
|
+
/* @__PURE__ */ jsx(
|
|
65
|
+
Button,
|
|
66
|
+
{
|
|
67
|
+
variant: "secondary",
|
|
68
|
+
size: "sm",
|
|
69
|
+
disabled: isRevokingAll,
|
|
70
|
+
onClick: () => setShowConfirmRevokeAll(false),
|
|
71
|
+
children: t("Cancel")
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
] }) : /* @__PURE__ */ jsx(
|
|
75
|
+
Button,
|
|
76
|
+
{
|
|
77
|
+
variant: "outline",
|
|
78
|
+
size: "sm",
|
|
79
|
+
onClick: () => setShowConfirmRevokeAll(true),
|
|
80
|
+
children: t("Revoke All Other Sessions")
|
|
81
|
+
}
|
|
82
|
+
))
|
|
83
|
+
] }),
|
|
84
|
+
/* @__PURE__ */ jsx(Typography, { variant: "secondary", type: "footnote", className: "mb-4", children: t("These are devices where you're currently logged in. You can revoke access to end a session.") }),
|
|
85
|
+
isLoading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-[300px] w-full rounded-md" }) : /* @__PURE__ */ jsx("div", { className: "border rounded-md", children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
86
|
+
/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
87
|
+
/* @__PURE__ */ jsx(TableHead, { className: "w-[200px]", children: t("Session") }),
|
|
88
|
+
/* @__PURE__ */ jsx(TableHead, { className: "w-[150px]", children: t("IP Address") }),
|
|
89
|
+
/* @__PURE__ */ jsx(TableHead, { className: "w-[150px]", children: t("Location") }),
|
|
90
|
+
/* @__PURE__ */ jsx(TableHead, { className: "w-[150px]", children: t("Last used") }),
|
|
91
|
+
/* @__PURE__ */ jsx(TableHead, { className: "w-[80px]" })
|
|
92
|
+
] }) }),
|
|
93
|
+
/* @__PURE__ */ jsx(TableBody, { children: sessions.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, { colSpan: 5, className: "text-center py-6", children: /* @__PURE__ */ jsx(Typography, { variant: "secondary", children: t("No active sessions found") }) }) }) : sessions.map((session) => /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
94
|
+
/* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
95
|
+
/* @__PURE__ */ jsx(Typography, { children: session.isCurrentSession ? t("Current Session") : t("Other Session") }),
|
|
96
|
+
session.isImpersonation && /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "w-fit mt-1", children: t("Impersonation") }),
|
|
97
|
+
/* @__PURE__ */ jsx(Typography, { variant: "secondary", type: "footnote", children: t("Signed in {time}", { time: new Date(session.createdAt).toLocaleDateString() }) })
|
|
98
|
+
] }) }),
|
|
99
|
+
/* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Typography, { children: session.geoInfo?.ip || t("-") }) }),
|
|
100
|
+
/* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Typography, { children: session.geoInfo?.cityName || t("Unknown") }) }),
|
|
101
|
+
/* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
102
|
+
/* @__PURE__ */ jsx(Typography, { children: session.lastUsedAt ? fromNow(new Date(session.lastUsedAt)) : t("Never") }),
|
|
103
|
+
/* @__PURE__ */ jsx(Typography, { variant: "secondary", type: "footnote", title: session.lastUsedAt ? new Date(session.lastUsedAt).toLocaleString() : "", children: session.lastUsedAt ? new Date(session.lastUsedAt).toLocaleDateString() : "" })
|
|
104
|
+
] }) }),
|
|
105
|
+
/* @__PURE__ */ jsx(TableCell, { align: "right", children: /* @__PURE__ */ jsx(
|
|
106
|
+
ActionCell,
|
|
107
|
+
{
|
|
108
|
+
items: [
|
|
109
|
+
{
|
|
110
|
+
item: t("Revoke"),
|
|
111
|
+
onClick: () => handleRevokeSession(session.id),
|
|
112
|
+
danger: true,
|
|
113
|
+
disabled: session.isCurrentSession,
|
|
114
|
+
disabledTooltip: session.isCurrentSession ? t("You cannot revoke your current session") : void 0
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
) })
|
|
119
|
+
] }, session.id)) })
|
|
120
|
+
] }) })
|
|
121
|
+
] }) });
|
|
122
|
+
}
|
|
123
|
+
export {
|
|
124
|
+
ActiveSessionsPage
|
|
125
|
+
};
|
|
126
|
+
//# sourceMappingURL=active-sessions-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/components-page/account-settings/active-sessions/active-sessions-page.tsx"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY\n//===========================================\nimport { fromNow } from \"@stackframe/stack-shared/dist/utils/dates\";\nimport { captureError } from \"@stackframe/stack-shared/dist/utils/errors\";\nimport { runAsynchronously } from \"@stackframe/stack-shared/dist/utils/promises\";\nimport { ActionCell, Badge, Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Typography } from \"@stackframe/stack-ui\";\nimport { useEffect, useState } from \"react\";\nimport { useUser } from \"../../../lib/hooks\";\nimport { ActiveSession } from \"../../../lib/stack-app/users\";\nimport { useTranslation } from \"../../../lib/translations\";\nimport { PageLayout } from \"../page-layout\";\n\nexport function ActiveSessionsPage() {\n const { t } = useTranslation();\n const user = useUser({ or: \"throw\" });\n const [isLoading, setIsLoading] = useState(true);\n const [isRevokingAll, setIsRevokingAll] = useState(false);\n const [sessions, setSessions] = useState<ActiveSession[]>([]);\n const [showConfirmRevokeAll, setShowConfirmRevokeAll] = useState(false);\n\n // Fetch sessions when component mounts\n useEffect(() => {\n runAsynchronously(async () => {\n setIsLoading(true);\n const sessionsData = await user.getActiveSessions();\n const enhancedSessions = sessionsData;\n setSessions(enhancedSessions);\n setIsLoading(false);\n });\n }, [user]);\n\n const handleRevokeSession = async (sessionId: string) => {\n try {\n await user.revokeSession(sessionId);\n\n // Remove the session from the list\n setSessions(prev => prev.filter(session => session.id !== sessionId));\n } catch (error) {\n captureError(\"Failed to revoke session\", { sessionId ,error });\n throw error;\n }\n };\n\n const handleRevokeAllSessions = async () => {\n setIsRevokingAll(true);\n try {\n const deletionPromises = sessions\n .filter(session => !session.isCurrentSession)\n .map(session => user.revokeSession(session.id));\n await Promise.all(deletionPromises);\n setSessions(prevSessions => prevSessions.filter(session => session.isCurrentSession));\n } catch (error) {\n captureError(\"Failed to revoke all sessions\", { error, sessionIds: sessions.map(session => session.id) });\n throw error;\n } finally {\n setIsRevokingAll(false);\n setShowConfirmRevokeAll(false);\n }\n };\n\n return (\n <PageLayout>\n <div>\n <div className=\"flex justify-between items-center mb-2\">\n <Typography className='font-medium'>{t(\"Active Sessions\")}</Typography>\n {sessions.filter(s => !s.isCurrentSession).length > 0 && !isLoading && (\n showConfirmRevokeAll ? (\n <div className=\"flex gap-2\">\n <Button\n variant=\"destructive\"\n size=\"sm\"\n loading={isRevokingAll}\n onClick={handleRevokeAllSessions}\n >\n {t(\"Confirm\")}\n </Button>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n disabled={isRevokingAll}\n onClick={() => setShowConfirmRevokeAll(false)}\n >\n {t(\"Cancel\")}\n </Button>\n </div>\n ) : (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setShowConfirmRevokeAll(true)}\n >\n {t(\"Revoke All Other Sessions\")}\n </Button>\n )\n )}\n </div>\n <Typography variant='secondary' type='footnote' className=\"mb-4\">\n {t(\"These are devices where you're currently logged in. You can revoke access to end a session.\")}\n </Typography>\n\n {isLoading ? (\n <Skeleton className=\"h-[300px] w-full rounded-md\" />\n ) : (\n <div className='border rounded-md'>\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"w-[200px]\">{t(\"Session\")}</TableHead>\n <TableHead className=\"w-[150px]\">{t(\"IP Address\")}</TableHead>\n <TableHead className=\"w-[150px]\">{t(\"Location\")}</TableHead>\n <TableHead className=\"w-[150px]\">{t(\"Last used\")}</TableHead>\n <TableHead className=\"w-[80px]\"></TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {sessions.length === 0 ? (\n <TableRow>\n <TableCell colSpan={5} className=\"text-center py-6\">\n <Typography variant=\"secondary\">{t(\"No active sessions found\")}</Typography>\n </TableCell>\n </TableRow>\n ) : (\n sessions.map((session) => (\n <TableRow key={session.id}>\n <TableCell>\n <div className=\"flex flex-col\">\n {/* We currently do not save any usefull information about the user, in the future, the name should probably say what kind of session it is (e.g. cli, browser, maybe what auth method was used) */}\n <Typography>{session.isCurrentSession ? t(\"Current Session\") : t(\"Other Session\")}</Typography>\n {session.isImpersonation && <Badge variant=\"secondary\" className=\"w-fit mt-1\">{t(\"Impersonation\")}</Badge>}\n <Typography variant='secondary' type='footnote'>\n {t(\"Signed in {time}\", { time: new Date(session.createdAt).toLocaleDateString() })}\n </Typography>\n </div>\n </TableCell>\n <TableCell>\n <Typography>{session.geoInfo?.ip || t('-')}</Typography>\n </TableCell>\n <TableCell>\n <Typography>{session.geoInfo?.cityName || t('Unknown')}</Typography>\n </TableCell>\n <TableCell>\n <div className=\"flex flex-col\">\n <Typography>{session.lastUsedAt ? fromNow(new Date(session.lastUsedAt)) : t(\"Never\")}</Typography>\n <Typography variant='secondary' type='footnote' title={session.lastUsedAt ? new Date(session.lastUsedAt).toLocaleString() : \"\"}>\n {session.lastUsedAt ? new Date(session.lastUsedAt).toLocaleDateString() : \"\"}\n </Typography>\n </div>\n </TableCell>\n <TableCell align=\"right\">\n <ActionCell\n items={[\n {\n item: t(\"Revoke\"),\n onClick: () => handleRevokeSession(session.id),\n danger: true,\n disabled: session.isCurrentSession,\n disabledTooltip: session.isCurrentSession ? t(\"You cannot revoke your current session\") : undefined,\n },\n ]}\n />\n </TableCell>\n </TableRow>\n ))\n )}\n </TableBody>\n </Table>\n </div>\n )}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/components-page/account-settings/api-keys/api-keys-page.tsx
|
|
2
|
+
import { Button } from "@stackframe/stack-ui";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { CreateApiKeyDialog, ShowApiKeyDialog } from "../../../components/api-key-dialogs";
|
|
5
|
+
import { ApiKeyTable } from "../../../components/api-key-table";
|
|
6
|
+
import { useUser } from "../../../lib/hooks";
|
|
7
|
+
import { useTranslation } from "../../../lib/translations";
|
|
8
|
+
import { PageLayout } from "../page-layout";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
function ApiKeysPage() {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const user = useUser({ or: "redirect" });
|
|
13
|
+
const apiKeys = user.useApiKeys();
|
|
14
|
+
const [isNewApiKeyDialogOpen, setIsNewApiKeyDialogOpen] = useState(false);
|
|
15
|
+
const [returnedApiKey, setReturnedApiKey] = useState(null);
|
|
16
|
+
const CreateDialog = CreateApiKeyDialog;
|
|
17
|
+
const ShowDialog = ShowApiKeyDialog;
|
|
18
|
+
return /* @__PURE__ */ jsxs(PageLayout, { children: [
|
|
19
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => setIsNewApiKeyDialogOpen(true), children: t("Create API Key") }),
|
|
20
|
+
/* @__PURE__ */ jsx(ApiKeyTable, { apiKeys }),
|
|
21
|
+
/* @__PURE__ */ jsx(
|
|
22
|
+
CreateDialog,
|
|
23
|
+
{
|
|
24
|
+
open: isNewApiKeyDialogOpen,
|
|
25
|
+
onOpenChange: setIsNewApiKeyDialogOpen,
|
|
26
|
+
onKeyCreated: setReturnedApiKey,
|
|
27
|
+
createApiKey: async (data) => {
|
|
28
|
+
const apiKey = await user.createApiKey(data);
|
|
29
|
+
return apiKey;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
),
|
|
33
|
+
/* @__PURE__ */ jsx(
|
|
34
|
+
ShowDialog,
|
|
35
|
+
{
|
|
36
|
+
apiKey: returnedApiKey,
|
|
37
|
+
onClose: () => setReturnedApiKey(null)
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
] });
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
ApiKeysPage
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=api-keys-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/components-page/account-settings/api-keys/api-keys-page.tsx"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY\n//===========================================\nimport { Button } from \"@stackframe/stack-ui\";\nimport { useState } from \"react\";\nimport { CreateApiKeyDialog, ShowApiKeyDialog } from \"../../../components/api-key-dialogs\";\nimport { ApiKeyTable } from \"../../../components/api-key-table\";\nimport { useUser } from \"../../../lib/hooks\";\nimport { ApiKey, ApiKeyCreationOptions } from \"../../../lib/stack-app/api-keys\";\nimport { useTranslation } from \"../../../lib/translations\";\nimport { PageLayout } from \"../page-layout\";\n\n\nexport function ApiKeysPage() {\n const { t } = useTranslation();\n\n const user = useUser({ or: 'redirect' });\n const apiKeys = user.useApiKeys();\n\n const [isNewApiKeyDialogOpen, setIsNewApiKeyDialogOpen] = useState(false);\n const [returnedApiKey, setReturnedApiKey] = useState<ApiKey<\"user\", true> | null>(null);\n\n const CreateDialog = CreateApiKeyDialog<\"user\">;\n const ShowDialog = ShowApiKeyDialog<\"user\">;\n\n return (\n <PageLayout>\n <Button onClick={() => setIsNewApiKeyDialogOpen(true)}>\n {t(\"Create API Key\")}\n </Button>\n <ApiKeyTable apiKeys={apiKeys} />\n <CreateDialog\n open={isNewApiKeyDialogOpen}\n onOpenChange={setIsNewApiKeyDialogOpen}\n onKeyCreated={setReturnedApiKey}\n createApiKey={async (data: ApiKeyCreationOptions<\"user\">) => {\n const apiKey = await user.createApiKey(data);\n return apiKey;\n }}\n />\n <ShowDialog\n apiKey={returnedApiKey}\n onClose={() => setReturnedApiKey(null)}\n />\n </PageLayout>\n );\n}\n"],"mappings":";AAIA,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,oBAAoB,wBAAwB;AACrD,SAAS,mBAAmB;AAC5B,SAAS,eAAe;AAExB,SAAS,sBAAsB;AAC/B,SAAS,kBAAkB;AAgBvB,SACE,KADF;AAbG,SAAS,cAAc;AAC5B,QAAM,EAAE,EAAE,IAAI,eAAe;AAE7B,QAAM,OAAO,QAAQ,EAAE,IAAI,WAAW,CAAC;AACvC,QAAM,UAAU,KAAK,WAAW;AAEhC,QAAM,CAAC,uBAAuB,wBAAwB,IAAI,SAAS,KAAK;AACxE,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAwC,IAAI;AAExF,QAAM,eAAe;AACrB,QAAM,aAAa;AAEnB,SACE,qBAAC,cACC;AAAA,wBAAC,UAAO,SAAS,MAAM,yBAAyB,IAAI,GACjD,YAAE,gBAAgB,GACrB;AAAA,IACA,oBAAC,eAAY,SAAkB;AAAA,IAC/B;AAAA,MAAC;AAAA;AAAA,QACC,MAAM;AAAA,QACN,cAAc;AAAA,QACd,cAAc;AAAA,QACd,cAAc,OAAO,SAAwC;AAC3D,gBAAM,SAAS,MAAM,KAAK,aAAa,IAAI;AAC3C,iBAAO;AAAA,QACT;AAAA;AAAA,IACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,QAAQ;AAAA,QACR,SAAS,MAAM,kBAAkB,IAAI;AAAA;AAAA,IACvC;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/components-page/account-settings/editable-text.tsx
|
|
2
|
+
import { Button, Input, Typography } from "@stackframe/stack-ui";
|
|
3
|
+
import { Edit } from "lucide-react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { useTranslation } from "../../lib/translations";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
function EditableText(props) {
|
|
8
|
+
const [editing, setEditing] = useState(false);
|
|
9
|
+
const [editingValue, setEditingValue] = useState(props.value);
|
|
10
|
+
const { t } = useTranslation();
|
|
11
|
+
return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
12
|
+
/* @__PURE__ */ jsx(
|
|
13
|
+
Input,
|
|
14
|
+
{
|
|
15
|
+
value: editingValue,
|
|
16
|
+
onChange: (e) => setEditingValue(e.target.value)
|
|
17
|
+
}
|
|
18
|
+
),
|
|
19
|
+
/* @__PURE__ */ jsx(
|
|
20
|
+
Button,
|
|
21
|
+
{
|
|
22
|
+
size: "sm",
|
|
23
|
+
onClick: async () => {
|
|
24
|
+
await props.onSave?.(editingValue);
|
|
25
|
+
setEditing(false);
|
|
26
|
+
},
|
|
27
|
+
children: t("Save")
|
|
28
|
+
}
|
|
29
|
+
),
|
|
30
|
+
/* @__PURE__ */ jsx(
|
|
31
|
+
Button,
|
|
32
|
+
{
|
|
33
|
+
size: "sm",
|
|
34
|
+
variant: "secondary",
|
|
35
|
+
onClick: () => {
|
|
36
|
+
setEditingValue(props.value);
|
|
37
|
+
setEditing(false);
|
|
38
|
+
},
|
|
39
|
+
children: t("Cancel")
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
43
|
+
/* @__PURE__ */ jsx(Typography, { children: props.value }),
|
|
44
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => setEditing(true), size: "icon", variant: "ghost", children: /* @__PURE__ */ jsx(Edit, { className: "w-4 h-4" }) })
|
|
45
|
+
] }) });
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
EditableText
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=editable-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/components-page/account-settings/editable-text.tsx"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY\n//===========================================\nimport { Button, Input, Typography } from \"@stackframe/stack-ui\";\nimport { Edit } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { useTranslation } from \"../../lib/translations\";\n\n\nexport function EditableText(props: { value: string, onSave?: (value: string) => void | Promise<void> }) {\n const [editing, setEditing] = useState(false);\n const [editingValue, setEditingValue] = useState(props.value);\n const { t } = useTranslation();\n\n return (\n <div className='flex items-center gap-2'>\n {editing ? (\n <>\n <Input\n value={editingValue}\n onChange={(e) => setEditingValue(e.target.value)}\n />\n <Button\n size='sm'\n onClick={async () => {\n await props.onSave?.(editingValue);\n setEditing(false);\n }}\n >\n {t(\"Save\")}\n </Button>\n <Button\n size='sm'\n variant='secondary'\n onClick={() => {\n setEditingValue(props.value);\n setEditing(false);\n }}>\n {t(\"Cancel\")}\n </Button>\n </>\n ) : (\n <>\n <Typography>{props.value}</Typography>\n <Button onClick={() => setEditing(true)} size='icon' variant='ghost'>\n <Edit className=\"w-4 h-4\" />\n </Button>\n </>\n )}\n </div>\n );\n}\n"],"mappings":";AAIA,SAAS,QAAQ,OAAO,kBAAkB;AAC1C,SAAS,YAAY;AACrB,SAAS,gBAAgB;AACzB,SAAS,sBAAsB;AAWvB,mBACE,KADF;AARD,SAAS,aAAa,OAA4E;AACvG,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,MAAM,KAAK;AAC5D,QAAM,EAAE,EAAE,IAAI,eAAe;AAE7B,SACE,oBAAC,SAAI,WAAU,2BACZ,oBACC,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,QACP,UAAU,CAAC,MAAM,gBAAgB,EAAE,OAAO,KAAK;AAAA;AAAA,IACjD;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,YAAY;AACnB,gBAAM,MAAM,SAAS,YAAY;AACjC,qBAAW,KAAK;AAAA,QAClB;AAAA,QAEC,YAAE,MAAM;AAAA;AAAA,IACX;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,SAAS,MAAM;AACb,0BAAgB,MAAM,KAAK;AAC3B,qBAAW,KAAK;AAAA,QAClB;AAAA,QACC,YAAE,QAAQ;AAAA;AAAA,IACb;AAAA,KACF,IAEA,iCACE;AAAA,wBAAC,cAAY,gBAAM,OAAM;AAAA,IACzB,oBAAC,UAAO,SAAS,MAAM,WAAW,IAAI,GAAG,MAAK,QAAO,SAAQ,SAC3D,8BAAC,QAAK,WAAU,WAAU,GAC5B;AAAA,KACF,GAEJ;AAEJ;","names":[]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/components-page/account-settings/email-and-auth/email-and-auth-page.tsx
|
|
2
|
+
import { PageLayout } from "../page-layout";
|
|
3
|
+
import { EmailsSection } from "./emails-section";
|
|
4
|
+
import { MfaSection } from "./mfa-section";
|
|
5
|
+
import { OtpSection } from "./otp-section";
|
|
6
|
+
import { PasskeySection } from "./passkey-section";
|
|
7
|
+
import { PasswordSection } from "./password-section";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
function EmailsAndAuthPage() {
|
|
10
|
+
return /* @__PURE__ */ jsxs(PageLayout, { children: [
|
|
11
|
+
/* @__PURE__ */ jsx(EmailsSection, {}),
|
|
12
|
+
/* @__PURE__ */ jsx(PasswordSection, {}),
|
|
13
|
+
/* @__PURE__ */ jsx(PasskeySection, {}),
|
|
14
|
+
/* @__PURE__ */ jsx(OtpSection, {}),
|
|
15
|
+
/* @__PURE__ */ jsx(MfaSection, {})
|
|
16
|
+
] });
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
EmailsAndAuthPage
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=email-and-auth-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/components-page/account-settings/email-and-auth/email-and-auth-page.tsx"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY\n//===========================================\nimport { PageLayout } from \"../page-layout\";\nimport { EmailsSection } from \"./emails-section\";\nimport { MfaSection } from \"./mfa-section\";\nimport { OtpSection } from \"./otp-section\";\nimport { PasskeySection } from \"./passkey-section\";\nimport { PasswordSection } from \"./password-section\";\n\nexport function EmailsAndAuthPage() {\n return (\n <PageLayout>\n <EmailsSection/>\n <PasswordSection />\n <PasskeySection />\n <OtpSection />\n <MfaSection />\n </PageLayout>\n );\n}\n"],"mappings":";AAIA,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB;AAI5B,SACE,KADF;AAFG,SAAS,oBAAoB;AAClC,SACE,qBAAC,cACC;AAAA,wBAAC,iBAAa;AAAA,IACd,oBAAC,mBAAgB;AAAA,IACjB,oBAAC,kBAAe;AAAA,IAChB,oBAAC,cAAW;AAAA,IACZ,oBAAC,cAAW;AAAA,KACd;AAEJ;","names":[]}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// src/components-page/account-settings/email-and-auth/emails-section.tsx
|
|
2
|
+
import { yupResolver } from "@hookform/resolvers/yup";
|
|
3
|
+
import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors";
|
|
4
|
+
import { strictEmailSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields";
|
|
5
|
+
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
|
|
6
|
+
import { ActionCell, Badge, Button, Input, Table, TableBody, TableCell, TableRow, Typography } from "@stackframe/stack-ui";
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
import { useForm } from "react-hook-form";
|
|
9
|
+
import { FormWarningText } from "../../../components/elements/form-warning";
|
|
10
|
+
import { useUser } from "../../../lib/hooks";
|
|
11
|
+
import { useTranslation } from "../../../lib/translations";
|
|
12
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
function EmailsSection() {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const user = useUser({ or: "redirect" });
|
|
16
|
+
const contactChannels = user.useContactChannels();
|
|
17
|
+
const [addingEmail, setAddingEmail] = useState(contactChannels.length === 0);
|
|
18
|
+
const [addingEmailLoading, setAddingEmailLoading] = useState(false);
|
|
19
|
+
const [addedEmail, setAddedEmail] = useState(null);
|
|
20
|
+
const isLastEmail = contactChannels.filter((x) => x.usedForAuth && x.type === "email").length === 1;
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (addedEmail) {
|
|
23
|
+
runAsynchronously(async () => {
|
|
24
|
+
const cc = contactChannels.find((x) => x.value === addedEmail);
|
|
25
|
+
if (cc && !cc.isVerified) {
|
|
26
|
+
await cc.sendVerificationEmail();
|
|
27
|
+
}
|
|
28
|
+
setAddedEmail(null);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}, [contactChannels, addedEmail]);
|
|
32
|
+
const emailSchema = yupObject({
|
|
33
|
+
email: strictEmailSchema(t("Please enter a valid email address")).notOneOf(contactChannels.map((x) => x.value), t("Email already exists")).defined().nonEmpty(t("Email is required"))
|
|
34
|
+
});
|
|
35
|
+
const { register, handleSubmit, formState: { errors }, reset } = useForm({
|
|
36
|
+
resolver: yupResolver(emailSchema)
|
|
37
|
+
});
|
|
38
|
+
const onSubmit = async (data) => {
|
|
39
|
+
setAddingEmailLoading(true);
|
|
40
|
+
try {
|
|
41
|
+
await user.createContactChannel({ type: "email", value: data.email, usedForAuth: false });
|
|
42
|
+
setAddedEmail(data.email);
|
|
43
|
+
} finally {
|
|
44
|
+
setAddingEmailLoading(false);
|
|
45
|
+
}
|
|
46
|
+
setAddingEmail(false);
|
|
47
|
+
reset();
|
|
48
|
+
};
|
|
49
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
50
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col md:flex-row justify-between mb-4 gap-4", children: [
|
|
51
|
+
/* @__PURE__ */ jsx(Typography, { className: "font-medium", children: t("Emails") }),
|
|
52
|
+
addingEmail ? /* @__PURE__ */ jsxs(
|
|
53
|
+
"form",
|
|
54
|
+
{
|
|
55
|
+
onSubmit: (e) => {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
runAsynchronously(handleSubmit(onSubmit));
|
|
58
|
+
},
|
|
59
|
+
className: "flex flex-col",
|
|
60
|
+
children: [
|
|
61
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
62
|
+
/* @__PURE__ */ jsx(
|
|
63
|
+
Input,
|
|
64
|
+
{
|
|
65
|
+
...register("email"),
|
|
66
|
+
placeholder: t("Enter email")
|
|
67
|
+
}
|
|
68
|
+
),
|
|
69
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", loading: addingEmailLoading, children: t("Add") }),
|
|
70
|
+
/* @__PURE__ */ jsx(
|
|
71
|
+
Button,
|
|
72
|
+
{
|
|
73
|
+
variant: "secondary",
|
|
74
|
+
onClick: () => {
|
|
75
|
+
setAddingEmail(false);
|
|
76
|
+
reset();
|
|
77
|
+
},
|
|
78
|
+
children: t("Cancel")
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
] }),
|
|
82
|
+
errors.email && /* @__PURE__ */ jsx(FormWarningText, { text: errors.email.message })
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
) : /* @__PURE__ */ jsx("div", { className: "flex md:justify-end", children: /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => setAddingEmail(true), children: t("Add an email") }) })
|
|
86
|
+
] }),
|
|
87
|
+
contactChannels.length > 0 ? /* @__PURE__ */ jsx("div", { className: "border rounded-md", children: /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsx(TableBody, { children: contactChannels.filter((x) => x.type === "email").sort((a, b) => {
|
|
88
|
+
if (a.isPrimary !== b.isPrimary) return a.isPrimary ? -1 : 1;
|
|
89
|
+
if (a.isVerified !== b.isVerified) return a.isVerified ? -1 : 1;
|
|
90
|
+
return 0;
|
|
91
|
+
}).map((x) => /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
92
|
+
/* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col md:flex-row gap-2 md:gap-4", children: [
|
|
93
|
+
x.value,
|
|
94
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
95
|
+
x.isPrimary ? /* @__PURE__ */ jsx(Badge, { children: t("Primary") }) : null,
|
|
96
|
+
!x.isVerified ? /* @__PURE__ */ jsx(Badge, { variant: "destructive", children: t("Unverified") }) : null,
|
|
97
|
+
x.usedForAuth ? /* @__PURE__ */ jsx(Badge, { variant: "outline", children: t("Used for sign-in") }) : null
|
|
98
|
+
] })
|
|
99
|
+
] }) }),
|
|
100
|
+
/* @__PURE__ */ jsx(TableCell, { className: "flex justify-end", children: /* @__PURE__ */ jsx(ActionCell, { items: [
|
|
101
|
+
...!x.isVerified ? [{
|
|
102
|
+
item: t("Send verification email"),
|
|
103
|
+
onClick: async () => {
|
|
104
|
+
await x.sendVerificationEmail();
|
|
105
|
+
}
|
|
106
|
+
}] : [],
|
|
107
|
+
...!x.isPrimary && x.isVerified ? [{
|
|
108
|
+
item: t("Set as primary"),
|
|
109
|
+
onClick: async () => {
|
|
110
|
+
await x.update({ isPrimary: true });
|
|
111
|
+
}
|
|
112
|
+
}] : !x.isPrimary ? [{
|
|
113
|
+
item: t("Set as primary"),
|
|
114
|
+
onClick: async () => {
|
|
115
|
+
},
|
|
116
|
+
disabled: true,
|
|
117
|
+
disabledTooltip: t("Please verify your email first")
|
|
118
|
+
}] : [],
|
|
119
|
+
...!x.usedForAuth && x.isVerified ? [{
|
|
120
|
+
item: t("Use for sign-in"),
|
|
121
|
+
onClick: async () => {
|
|
122
|
+
try {
|
|
123
|
+
await x.update({ usedForAuth: true });
|
|
124
|
+
} catch (e) {
|
|
125
|
+
if (e instanceof KnownErrors.ContactChannelAlreadyUsedForAuthBySomeoneElse) {
|
|
126
|
+
alert(t("This email is already used for sign-in by another user."));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}] : [],
|
|
131
|
+
...x.usedForAuth && !isLastEmail ? [{
|
|
132
|
+
item: t("Stop using for sign-in"),
|
|
133
|
+
onClick: async () => {
|
|
134
|
+
await x.update({ usedForAuth: false });
|
|
135
|
+
}
|
|
136
|
+
}] : x.usedForAuth ? [{
|
|
137
|
+
item: t("Stop using for sign-in"),
|
|
138
|
+
onClick: async () => {
|
|
139
|
+
},
|
|
140
|
+
disabled: true,
|
|
141
|
+
disabledTooltip: t("You can not remove your last sign-in email")
|
|
142
|
+
}] : [],
|
|
143
|
+
...!isLastEmail || !x.usedForAuth ? [{
|
|
144
|
+
item: t("Remove"),
|
|
145
|
+
onClick: async () => {
|
|
146
|
+
await x.delete();
|
|
147
|
+
},
|
|
148
|
+
danger: true
|
|
149
|
+
}] : [{
|
|
150
|
+
item: t("Remove"),
|
|
151
|
+
onClick: async () => {
|
|
152
|
+
},
|
|
153
|
+
disabled: true,
|
|
154
|
+
disabledTooltip: t("You can not remove your last sign-in email")
|
|
155
|
+
}]
|
|
156
|
+
] }) })
|
|
157
|
+
] }, x.id)) }) }) }) : null
|
|
158
|
+
] });
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
EmailsSection
|
|
162
|
+
};
|
|
163
|
+
//# sourceMappingURL=emails-section.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/components-page/account-settings/email-and-auth/emails-section.tsx"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY\n//===========================================\nimport { yupResolver } from \"@hookform/resolvers/yup\";\nimport { KnownErrors } from \"@stackframe/stack-shared/dist/known-errors\";\nimport { strictEmailSchema, yupObject } from \"@stackframe/stack-shared/dist/schema-fields\";\nimport { runAsynchronously } from \"@stackframe/stack-shared/dist/utils/promises\";\nimport { ActionCell, Badge, Button, Input, Table, TableBody, TableCell, TableRow, Typography } from \"@stackframe/stack-ui\";\nimport { useEffect, useState } from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport * as yup from \"yup\";\nimport { FormWarningText } from \"../../../components/elements/form-warning\";\nimport { useUser } from \"../../../lib/hooks\";\nimport { useTranslation } from \"../../../lib/translations\";\n\nexport function EmailsSection() {\n const { t } = useTranslation();\n const user = useUser({ or: 'redirect' });\n const contactChannels = user.useContactChannels();\n const [addingEmail, setAddingEmail] = useState(contactChannels.length === 0);\n const [addingEmailLoading, setAddingEmailLoading] = useState(false);\n const [addedEmail, setAddedEmail] = useState<string | null>(null);\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const isLastEmail = contactChannels.filter(x => x.usedForAuth && x.type === 'email').length === 1;\n\n useEffect(() => {\n if (addedEmail) {\n runAsynchronously(async () => {\n const cc = contactChannels.find(x => x.value === addedEmail);\n if (cc && !cc.isVerified) {\n await cc.sendVerificationEmail();\n }\n setAddedEmail(null);\n });\n }\n }, [contactChannels, addedEmail]);\n\n const emailSchema = yupObject({\n email: strictEmailSchema(t('Please enter a valid email address'))\n .notOneOf(contactChannels.map(x => x.value), t('Email already exists'))\n .defined()\n .nonEmpty(t('Email is required')),\n });\n\n const { register, handleSubmit, formState: { errors }, reset } = useForm({\n resolver: yupResolver(emailSchema)\n });\n\n const onSubmit = async (data: yup.InferType<typeof emailSchema>) => {\n setAddingEmailLoading(true);\n try {\n await user.createContactChannel({ type: 'email', value: data.email, usedForAuth: false });\n setAddedEmail(data.email);\n } finally {\n setAddingEmailLoading(false);\n }\n setAddingEmail(false);\n reset();\n };\n\n return (\n <div>\n <div className='flex flex-col md:flex-row justify-between mb-4 gap-4'>\n <Typography className='font-medium'>{t(\"Emails\")}</Typography>\n {addingEmail ? (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n runAsynchronously(handleSubmit(onSubmit));\n }}\n className='flex flex-col'\n >\n <div className='flex gap-2'>\n <Input\n {...register(\"email\")}\n placeholder={t(\"Enter email\")}\n />\n <Button type=\"submit\" loading={addingEmailLoading}>\n {t(\"Add\")}\n </Button>\n <Button\n variant='secondary'\n onClick={() => {\n setAddingEmail(false);\n reset();\n }}\n >\n {t(\"Cancel\")}\n </Button>\n </div>\n {errors.email && <FormWarningText text={errors.email.message} />}\n </form>\n ) : (\n <div className='flex md:justify-end'>\n <Button variant='secondary' onClick={() => setAddingEmail(true)}>{t(\"Add an email\")}</Button>\n </div>\n )}\n </div>\n\n {contactChannels.length > 0 ? (\n <div className='border rounded-md'>\n <Table>\n <TableBody>\n {/*eslint-disable-next-line @typescript-eslint/no-unnecessary-condition*/}\n {contactChannels.filter(x => x.type === 'email')\n .sort((a, b) => {\n if (a.isPrimary !== b.isPrimary) return a.isPrimary ? -1 : 1;\n if (a.isVerified !== b.isVerified) return a.isVerified ? -1 : 1;\n return 0;\n })\n .map(x => (\n <TableRow key={x.id}>\n <TableCell>\n <div className='flex flex-col md:flex-row gap-2 md:gap-4'>\n {x.value}\n <div className='flex gap-2'>\n {x.isPrimary ? <Badge>{t(\"Primary\")}</Badge> : null}\n {!x.isVerified ? <Badge variant='destructive'>{t(\"Unverified\")}</Badge> : null}\n {x.usedForAuth ? <Badge variant='outline'>{t(\"Used for sign-in\")}</Badge> : null}\n </div>\n </div>\n </TableCell>\n <TableCell className=\"flex justify-end\">\n <ActionCell items={[\n ...(!x.isVerified ? [{\n item: t(\"Send verification email\"),\n onClick: async () => { await x.sendVerificationEmail(); },\n }] : []),\n ...(!x.isPrimary && x.isVerified ? [{\n item: t(\"Set as primary\"),\n onClick: async () => { await x.update({ isPrimary: true }); },\n }] :\n !x.isPrimary ? [{\n item: t(\"Set as primary\"),\n onClick: async () => {},\n disabled: true,\n disabledTooltip: t(\"Please verify your email first\"),\n }] : []),\n ...(!x.usedForAuth && x.isVerified ? [{\n item: t(\"Use for sign-in\"),\n onClick: async () => {\n try {\n await x.update({ usedForAuth: true });\n } catch (e) {\n if (e instanceof KnownErrors.ContactChannelAlreadyUsedForAuthBySomeoneElse) {\n alert(t(\"This email is already used for sign-in by another user.\"));\n }\n }\n }\n }] : []),\n ...(x.usedForAuth && !isLastEmail ? [{\n item: t(\"Stop using for sign-in\"),\n onClick: async () => { await x.update({ usedForAuth: false }); },\n }] : x.usedForAuth ? [{\n item: t(\"Stop using for sign-in\"),\n onClick: async () => {},\n disabled: true,\n disabledTooltip: t(\"You can not remove your last sign-in email\"),\n }] : []),\n ...(!isLastEmail || !x.usedForAuth ? [{\n item: t(\"Remove\"),\n onClick: async () => { await x.delete(); },\n danger: true,\n }] : [{\n item: t(\"Remove\"),\n onClick: async () => {},\n disabled: true,\n disabledTooltip: t(\"You can not remove your last sign-in email\"),\n }]),\n ]}/>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// src/components-page/account-settings/email-and-auth/mfa-section.tsx
|
|
2
|
+
import { createTOTPKeyURI, verifyTOTP } from "@oslojs/otp";
|
|
3
|
+
import { useAsyncCallback } from "@stackframe/stack-shared/dist/hooks/use-async-callback";
|
|
4
|
+
import { generateRandomValues } from "@stackframe/stack-shared/dist/utils/crypto";
|
|
5
|
+
import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
|
6
|
+
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
|
|
7
|
+
import { Button, Input, Typography } from "@stackframe/stack-ui";
|
|
8
|
+
import * as QRCode from "qrcode";
|
|
9
|
+
import { useEffect, useState } from "react";
|
|
10
|
+
import { useStackApp, useUser } from "../../../lib/hooks";
|
|
11
|
+
import { useTranslation } from "../../../lib/translations";
|
|
12
|
+
import { Section } from "../section";
|
|
13
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
function MfaSection() {
|
|
15
|
+
const { t } = useTranslation();
|
|
16
|
+
const project = useStackApp().useProject();
|
|
17
|
+
const user = useUser({ or: "throw" });
|
|
18
|
+
const [generatedSecret, setGeneratedSecret] = useState(null);
|
|
19
|
+
const [qrCodeUrl, setQrCodeUrl] = useState(null);
|
|
20
|
+
const [mfaCode, setMfaCode] = useState("");
|
|
21
|
+
const [isMaybeWrong, setIsMaybeWrong] = useState(false);
|
|
22
|
+
const isEnabled = user.isMultiFactorRequired;
|
|
23
|
+
const [handleSubmit, isLoading] = useAsyncCallback(async () => {
|
|
24
|
+
await user.update({
|
|
25
|
+
totpMultiFactorSecret: generatedSecret
|
|
26
|
+
});
|
|
27
|
+
setGeneratedSecret(null);
|
|
28
|
+
setQrCodeUrl(null);
|
|
29
|
+
setMfaCode("");
|
|
30
|
+
}, [generatedSecret, user]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setIsMaybeWrong(false);
|
|
33
|
+
runAsynchronouslyWithAlert(async () => {
|
|
34
|
+
if (generatedSecret && verifyTOTP(generatedSecret, 30, 6, mfaCode)) {
|
|
35
|
+
await handleSubmit();
|
|
36
|
+
}
|
|
37
|
+
setIsMaybeWrong(true);
|
|
38
|
+
});
|
|
39
|
+
}, [mfaCode, generatedSecret, handleSubmit]);
|
|
40
|
+
return /* @__PURE__ */ jsx(
|
|
41
|
+
Section,
|
|
42
|
+
{
|
|
43
|
+
title: t("Multi-factor authentication"),
|
|
44
|
+
description: isEnabled ? t("Multi-factor authentication is currently enabled.") : t("Multi-factor authentication is currently disabled."),
|
|
45
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
46
|
+
!isEnabled && generatedSecret && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
47
|
+
/* @__PURE__ */ jsx(Typography, { children: t("Scan this QR code with your authenticator app:") }),
|
|
48
|
+
/* @__PURE__ */ jsx("img", { width: 200, height: 200, src: qrCodeUrl ?? throwErr("TOTP QR code failed to generate"), alt: t("TOTP multi-factor authentication QR code") }),
|
|
49
|
+
/* @__PURE__ */ jsx(Typography, { children: t("Then, enter your six-digit MFA code:") }),
|
|
50
|
+
/* @__PURE__ */ jsx(
|
|
51
|
+
Input,
|
|
52
|
+
{
|
|
53
|
+
value: mfaCode,
|
|
54
|
+
onChange: (e) => {
|
|
55
|
+
setIsMaybeWrong(false);
|
|
56
|
+
setMfaCode(e.target.value);
|
|
57
|
+
},
|
|
58
|
+
placeholder: "123456",
|
|
59
|
+
maxLength: 6,
|
|
60
|
+
disabled: isLoading
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
isMaybeWrong && mfaCode.length === 6 && /* @__PURE__ */ jsx(Typography, { variant: "destructive", children: t("Incorrect code. Please try again.") }),
|
|
64
|
+
/* @__PURE__ */ jsx("div", { className: "flex", children: /* @__PURE__ */ jsx(
|
|
65
|
+
Button,
|
|
66
|
+
{
|
|
67
|
+
variant: "secondary",
|
|
68
|
+
onClick: () => {
|
|
69
|
+
setGeneratedSecret(null);
|
|
70
|
+
setQrCodeUrl(null);
|
|
71
|
+
setMfaCode("");
|
|
72
|
+
},
|
|
73
|
+
children: t("Cancel")
|
|
74
|
+
}
|
|
75
|
+
) })
|
|
76
|
+
] }),
|
|
77
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-2", children: isEnabled ? /* @__PURE__ */ jsx(
|
|
78
|
+
Button,
|
|
79
|
+
{
|
|
80
|
+
variant: "secondary",
|
|
81
|
+
onClick: async () => {
|
|
82
|
+
await user.update({
|
|
83
|
+
totpMultiFactorSecret: null
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
children: t("Disable MFA")
|
|
87
|
+
}
|
|
88
|
+
) : !generatedSecret && /* @__PURE__ */ jsx(
|
|
89
|
+
Button,
|
|
90
|
+
{
|
|
91
|
+
variant: "secondary",
|
|
92
|
+
onClick: async () => {
|
|
93
|
+
const secret = generateRandomValues(new Uint8Array(20));
|
|
94
|
+
setQrCodeUrl(await generateTotpQrCode(project, user, secret));
|
|
95
|
+
setGeneratedSecret(secret);
|
|
96
|
+
},
|
|
97
|
+
children: t("Enable MFA")
|
|
98
|
+
}
|
|
99
|
+
) })
|
|
100
|
+
] })
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
async function generateTotpQrCode(project, user, secret) {
|
|
105
|
+
const uri = createTOTPKeyURI(project.displayName, user.primaryEmail ?? user.id, secret, 30, 6);
|
|
106
|
+
return await QRCode.toDataURL(uri);
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
MfaSection
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=mfa-section.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/components-page/account-settings/email-and-auth/mfa-section.tsx"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY\n//===========================================\nimport { createTOTPKeyURI, verifyTOTP } from \"@oslojs/otp\";\nimport { useAsyncCallback } from '@stackframe/stack-shared/dist/hooks/use-async-callback';\nimport { generateRandomValues } from '@stackframe/stack-shared/dist/utils/crypto';\nimport { throwErr } from \"@stackframe/stack-shared/dist/utils/errors\";\nimport { runAsynchronouslyWithAlert } from \"@stackframe/stack-shared/dist/utils/promises\";\nimport { Button, Input, Typography } from \"@stackframe/stack-ui\";\nimport * as QRCode from 'qrcode';\nimport { useEffect, useState } from \"react\";\nimport { CurrentUser, Project } from '../../..';\nimport { useStackApp, useUser } from \"../../../lib/hooks\";\nimport { useTranslation } from \"../../../lib/translations\";\nimport { Section } from \"../section\";\n\nexport function MfaSection() {\n const { t } = useTranslation();\n const project = useStackApp().useProject();\n const user = useUser({ or: \"throw\" });\n const [generatedSecret, setGeneratedSecret] = useState<Uint8Array | null>(null);\n const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null);\n const [mfaCode, setMfaCode] = useState<string>(\"\");\n const [isMaybeWrong, setIsMaybeWrong] = useState(false);\n const isEnabled = user.isMultiFactorRequired;\n\n const [handleSubmit, isLoading] = useAsyncCallback(async () => {\n await user.update({\n totpMultiFactorSecret: generatedSecret,\n });\n setGeneratedSecret(null);\n setQrCodeUrl(null);\n setMfaCode(\"\");\n }, [generatedSecret, user]);\n\n useEffect(() => {\n setIsMaybeWrong(false);\n runAsynchronouslyWithAlert(async () => {\n if (generatedSecret && verifyTOTP(generatedSecret, 30, 6, mfaCode)) {\n await handleSubmit();\n }\n setIsMaybeWrong(true);\n });\n }, [mfaCode, generatedSecret, handleSubmit]);\n\n return (\n <Section\n title={t(\"Multi-factor authentication\")}\n description={isEnabled\n ? t(\"Multi-factor authentication is currently enabled.\")\n : t(\"Multi-factor authentication is currently disabled.\")}\n >\n <div className='flex flex-col gap-4'>\n {!isEnabled && generatedSecret && (\n <>\n <Typography>{t(\"Scan this QR code with your authenticator app:\")}</Typography>\n <img width={200} height={200} src={qrCodeUrl ?? throwErr(\"TOTP QR code failed to generate\")} alt={t(\"TOTP multi-factor authentication QR code\")} />\n <Typography>{t(\"Then, enter your six-digit MFA code:\")}</Typography>\n <Input\n value={mfaCode}\n onChange={(e) => {\n setIsMaybeWrong(false);\n setMfaCode(e.target.value);\n }}\n placeholder=\"123456\"\n maxLength={6}\n disabled={isLoading}\n />\n {isMaybeWrong && mfaCode.length === 6 && (\n <Typography variant=\"destructive\">{t(\"Incorrect code. Please try again.\")}</Typography>\n )}\n <div className='flex'>\n <Button\n variant='secondary'\n onClick={() => {\n setGeneratedSecret(null);\n setQrCodeUrl(null);\n setMfaCode(\"\");\n }}\n >\n {t(\"Cancel\")}\n </Button>\n </div>\n </>\n )}\n <div className='flex gap-2'>\n {isEnabled ? (\n <Button\n variant='secondary'\n onClick={async () => {\n await user.update({\n totpMultiFactorSecret: null,\n });\n }}\n >\n {t(\"Disable MFA\")}\n </Button>\n ) : !generatedSecret && (\n <Button\n variant='secondary'\n onClick={async () => {\n const secret = generateRandomValues(new Uint8Array(20));\n setQrCodeUrl(await generateTotpQrCode(project, user, secret));\n setGeneratedSecret(secret);\n }}\n >\n {t(\"Enable MFA\")}\n </Button>\n )}\n </div>\n </div>\n </Section>\n );\n}\n\n\nasync function generateTotpQrCode(project: Project, user: CurrentUser, secret: Uint8Array) {\n const uri = createTOTPKeyURI(project.displayName, user.primaryEmail ?? user.id, secret, 30, 6);\n return await QRCode.toDataURL(uri) as any;\n}\n"],"mappings":"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","names":[]}
|