diff --git a/infra/staff/src/App.css b/infra/staff/src/App.css index fd37de8350..e1c9e003de 100644 --- a/infra/staff/src/App.css +++ b/infra/staff/src/App.css @@ -304,3 +304,44 @@ .active-field input { width: 50px; } +/* Add to App.css or your CSS file */ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + min-height: 100vh; +} + +.input-form { + margin-bottom: 16px; +} + +.content-container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + margin-top: 300px; +} + +.horizontal-group { + display: flex; + align-items: center; +} + +.fetch-button-container { + margin-right: 10px; + margin-left: 10px; +} + +.text-field-token, +.text-field-email { + margin: 0 8px; +} + +.error-message { + color: red; + font-weight: bold; + margin-top: 16px; +} diff --git a/infra/staff/src/App.tsx b/infra/staff/src/App.tsx index f0a1eab075..b87f401271 100644 --- a/infra/staff/src/App.tsx +++ b/infra/staff/src/App.tsx @@ -7,12 +7,13 @@ import TextField from "@mui/material/TextField"; import * as React from "react"; import { useEffect, useState } from "react"; import "./App.css"; +import StorageBonusTableComponent from "./components/StorageBonusTableComponent"; +import FamilyTableComponent from "./components/FamilyComponentTable"; import type { UserData } from "./components/UserComponent"; import UserComponent from "./components/UserComponent"; import duckieimage from "./components/duckie.png"; import { apiOrigin } from "./services/support"; -// Define and export email and token variables and their setter functions export let email = ""; export let token = ""; @@ -43,7 +44,7 @@ interface Subscription { interface Security { isEmailMFAEnabled: boolean; isTwoFactorEnabled: boolean; - passkeys: string; // Replace with actual passkey value if available + passkeys: string; } interface UserResponse { @@ -57,8 +58,8 @@ interface UserResponse { } const App: React.FC = () => { - const [localEmail, setLocalEmail] = useState(getEmail()); - const [localToken, setLocalToken] = useState(getToken()); + const [localEmail, setLocalEmail] = useState(""); + const [localToken, setLocalToken] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [fetchSuccess, setFetchSuccess] = useState(false); @@ -91,14 +92,34 @@ const App: React.FC = () => { } }, [localEmail]); + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const urlEmail = urlParams.get("email"); + const urlToken = urlParams.get("token"); + + if (urlEmail && urlToken) { + setLocalEmail(urlEmail); + setLocalToken(urlToken); + console.log(localEmail); + console.log(localToken); + setEmail(urlEmail); + setToken(urlToken); + fetchData().catch((error: unknown) => + console.error("Fetch data error:", error), + ); + } + console.log(email); + console.log(token); + }, []); + const fetchData = async () => { setLoading(true); setError(""); setFetchSuccess(false); const startTime = Date.now(); try { - const encodedEmail = encodeURIComponent(localEmail); - const encodedToken = encodeURIComponent(localToken); + const encodedEmail = encodeURIComponent(email); + const encodedToken = encodeURIComponent(token); const url = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`; console.log(`Fetching data from URL: ${url}`); const response = await fetch(url); @@ -156,7 +177,7 @@ const App: React.FC = () => { .isTwoFactorEnabled ? "Enabled" : "Disabled", - Passkeys: "None", // Replace with actual passkey value if available + Passkeys: "None", }, }; @@ -195,7 +216,7 @@ const App: React.FC = () => { }; return ( -
+ diff --git a/infra/staff/src/components/CloseFamily.tsx b/infra/staff/src/components/CloseFamily.tsx new file mode 100644 index 0000000000..303d8b0dfe --- /dev/null +++ b/infra/staff/src/components/CloseFamily.tsx @@ -0,0 +1,157 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Paper, +} from "@mui/material"; +import React, { useState } from "react"; +import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions +import { apiOrigin } from "../services/support"; + +interface UserData { + subscription?: { + userID: string; + // Add other properties as per your API response structure + }; + // Add other properties as per your API response structure +} + +interface CloseFamilyProps { + open: boolean; + handleClose: () => void; + handleCloseFamily: () => void; // Callback to handle closing family +} + +const CloseFamily: React.FC = ({ + open, + handleClose, + handleCloseFamily, +}) => { + const [loading, setLoading] = useState(false); + + const handleClosure = async () => { + try { + setLoading(true); + const email = getEmail(); + const token = getToken(); + + if (!email) { + throw new Error("Email not found"); + } + + if (!token) { + throw new Error("Token not found"); + } + + const encodedEmail = encodeURIComponent(email); + const encodedToken = encodeURIComponent(token); + + // Fetch user data + const userUrl = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`; + const userResponse = await fetch(userUrl); + if (!userResponse.ok) { + throw new Error("Failed to fetch user data"); + } + const userData = (await userResponse.json()) as UserData; + const userId = userData.subscription?.userID; + + if (!userId) { + throw new Error("User ID not found"); + } + + // Close family action + const closeFamilyUrl = `${apiOrigin}/admin/user/close-family?token=${encodedToken}`; + const body = JSON.stringify({ userId }); + const closeFamilyResponse = await fetch(closeFamilyUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: body, + }); + + if (!closeFamilyResponse.ok) { + const errorResponse = await closeFamilyResponse.text(); + throw new Error(`Failed to close family: ${errorResponse}`); + } + + handleCloseFamily(); // Notify parent component of successful action + handleClose(); // Close dialog on successful action + console.log("Family closed successfully"); + } catch (error) { + console.error("Error closing family:", error); + } finally { + setLoading(false); + } + }; + + const handleCancel = () => { + handleClose(); // Close dialog + }; + + return ( +
+ + + {"Close Family?"} + + + + Are you sure you want to close family relations for this + account? + + + + + + + +
+ ); +}; + +export default CloseFamily; diff --git a/infra/staff/src/components/DisablePasskeys.tsx b/infra/staff/src/components/DisablePasskeys.tsx new file mode 100644 index 0000000000..175c9631f2 --- /dev/null +++ b/infra/staff/src/components/DisablePasskeys.tsx @@ -0,0 +1,157 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Paper, +} from "@mui/material"; +import React, { useState } from "react"; +import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions +import { apiOrigin } from "../services/support"; + +interface UserData { + subscription?: { + userID: string; + // Add other properties as per your API response structure + }; + // Add other properties as per your API response structure +} + +interface DisablePasskeysProps { + open: boolean; + handleClose: () => void; + handleDisablePasskeys: () => void; // Callback to handle disabling passkeys +} + +const DisablePasskeys: React.FC = ({ + open, + handleClose, + handleDisablePasskeys, +}) => { + const [loading, setLoading] = useState(false); + + const handleDisabling = async () => { + try { + setLoading(true); + const email = getEmail(); + const token = getToken(); + + if (!email) { + throw new Error("Email not found"); + } + + if (!token) { + throw new Error("Token not found"); + } + + const encodedEmail = encodeURIComponent(email); + const encodedToken = encodeURIComponent(token); + + // Fetch user data + const userUrl = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`; + const userResponse = await fetch(userUrl); + if (!userResponse.ok) { + throw new Error("Failed to fetch user data"); + } + const userData = (await userResponse.json()) as UserData; + const userId = userData.subscription?.userID; + + if (!userId) { + throw new Error("User ID not found"); + } + + // Disable passkeys action + const disablePasskeysUrl = `${apiOrigin}/admin/user/disable-passkeys?token=${encodedToken}`; + const body = JSON.stringify({ userId }); + const disablePasskeysResponse = await fetch(disablePasskeysUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: body, + }); + + if (!disablePasskeysResponse.ok) { + const errorResponse = await disablePasskeysResponse.text(); + throw new Error(`Failed to disable passkeys: ${errorResponse}`); + } + + handleDisablePasskeys(); // Notify parent component of successful action + handleClose(); // Close dialog on successful action + console.log("Passkeys disabled successfully"); + } catch (error) { + console.error("Error disabling passkeys:", error); + } finally { + setLoading(false); + } + }; + + const handleCancel = () => { + handleClose(); // Close dialog + }; + + return ( +
+ + + {"Disable Passkeys?"} + + + + Are you sure you want to disable passkeys for this + account? + + + + + + + +
+ ); +}; + +export default DisablePasskeys; diff --git a/infra/staff/src/components/FamilyComponentTable.tsx b/infra/staff/src/components/FamilyComponentTable.tsx new file mode 100644 index 0000000000..2035ecda28 --- /dev/null +++ b/infra/staff/src/components/FamilyComponentTable.tsx @@ -0,0 +1,176 @@ +import { + Button, + CircularProgress, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from "@mui/material"; +import * as React from "react"; +import { useEffect, useState } from "react"; +import { getEmail, getToken } from "../App"; +import { apiOrigin } from "../services/support"; +import CloseFamily from "./CloseFamily"; + +interface FamilyMember { + id: string; + email: string; + status: string; + usage: number; +} + +interface UserData { + details: { + familyData: { + members: FamilyMember[]; + }; + }; +} + +const FamilyTableComponent: React.FC = () => { + const [familyMembers, setFamilyMembers] = useState([]); + const [closeFamilyOpen, setCloseFamilyOpen] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const encodedEmail = encodeURIComponent(getEmail()); + const encodedToken = encodeURIComponent(getToken()); + const url = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`; + const response = await fetch(url); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + const userData = (await response.json()) as UserData; // Typecast to UserData interface + const members: FamilyMember[] = + userData.details.familyData.members; + setFamilyMembers(members); + } catch (error) { + console.error("Error fetching family data:", error); + setError("No family data"); + } finally { + setLoading(false); + } + }; + + fetchData().catch((error: unknown) => + console.error("Fetch data error:", error), + ); + }, []); + + const formatUsageToGB = (usage: number): string => { + const usageInGB = (usage / (1024 * 1024 * 1024)).toFixed(2); + return `${usageInGB} GB`; + }; + + const handleOpenCloseFamily = () => { + setCloseFamilyOpen(true); + }; + + const handleCloseCloseFamily = () => { + setCloseFamilyOpen(false); + }; + + const handleCloseFamily = () => { + console.log("Close family action"); + handleOpenCloseFamily(); + }; + + if (loading) { + return ; + } + + if (error) { + return
Error: {error}
; + } + + if (familyMembers.length === 0) { + return
No family data available
; + } + + return ( + <> + + + + + + User + + + Status + + + Usage + + + ID + + + + + {familyMembers.map((member) => ( + + {member.email} + + + {member.status === "SELF" + ? "ADMIN" + : member.status} + + + + {formatUsageToGB(member.usage)} + + {member.id} + + ))} + +
+
+
+ +
+ + {closeFamilyOpen && ( + + )} + + ); +}; + +export default FamilyTableComponent; diff --git a/infra/staff/src/components/StorageBonusTableComponent.tsx b/infra/staff/src/components/StorageBonusTableComponent.tsx new file mode 100644 index 0000000000..3b67c3ee2b --- /dev/null +++ b/infra/staff/src/components/StorageBonusTableComponent.tsx @@ -0,0 +1,160 @@ +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from "@mui/material"; +import * as React from "react"; +import { useEffect, useState } from "react"; +import { getEmail, getToken } from "../App"; +import { apiOrigin } from "../services/support"; + +interface BonusData { + storage: number; + type: string; + createdAt: number; + validTill: number; + isRevoked: boolean; +} + +interface UserData { + details: { + bonusData: { + storageBonuses: BonusData[]; + }; + }; +} + +const StorageBonusTableComponent: React.FC = () => { + const [storageBonuses, setStorageBonuses] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const encodedEmail = encodeURIComponent(getEmail()); + const encodedToken = encodeURIComponent(getToken()); + const url = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`; + const response = await fetch(url); + if (!response.ok) { + throw new Error("Failed to fetch bonus data"); + } + const userData = (await response.json()) as UserData; // Typecast to UserData interface + const bonuses: BonusData[] = + userData.details.bonusData.storageBonuses; + setStorageBonuses(bonuses); + } catch (error) { + console.error("Error fetching bonus data:", error); + setError("No bonus data"); + } finally { + setLoading(false); + } + }; + + fetchData().catch((error: unknown) => + console.error("Fetch data error:", error), + ); + }, []); + + const formatCreatedAt = (createdAt: number): string => { + const date = new Date(createdAt / 1000); + return date.toLocaleDateString(); // Adjust date formatting as needed + }; + + const formatValidTill = (validTill: number): string => { + if (validTill === 0) { + return "Forever"; + } else { + const date = new Date(validTill / 1000); + return date.toLocaleDateString(); // Adjust date formatting as needed + } + }; + + const formatStorage = (storage: number): string => { + const inGB = storage / (1024 * 1024 * 1024); + return `${inGB.toFixed(2)} GB`; + }; + + if (loading) { + return

Loading...

; + } + + if (error) { + return

Error: {error}

; + } + + if (storageBonuses.length === 0) { + return

No bonus data available

; + } + + return ( +
+ + + + + + Storage + + + Type + + + Created At + + + Valid Till + + + Is Revoked + + + + + {storageBonuses.map((bonus, index) => ( + + + {formatStorage(bonus.storage)} + + {bonus.type} + + {formatCreatedAt(bonus.createdAt)} + + + {formatValidTill(bonus.validTill)} + + + + {bonus.isRevoked ? "Yes" : "No"} + + + + ))} + +
+
+
+ ); +}; + +export default StorageBonusTableComponent; diff --git a/infra/staff/src/components/UpdateSubscription.tsx b/infra/staff/src/components/UpdateSubscription.tsx index f1b36e25c4..d3fef0d54b 100644 --- a/infra/staff/src/components/UpdateSubscription.tsx +++ b/infra/staff/src/components/UpdateSubscription.tsx @@ -246,6 +246,7 @@ const UpdateSubscription: React.FC = ({ Stripe PayPal BitPay + None
diff --git a/infra/staff/src/components/UserComponent.tsx b/infra/staff/src/components/UserComponent.tsx index 645572162b..ec794f10ef 100644 --- a/infra/staff/src/components/UserComponent.tsx +++ b/infra/staff/src/components/UserComponent.tsx @@ -1,6 +1,8 @@ import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; + import Grid from "@mui/material/Grid"; import IconButton from "@mui/material/IconButton"; import Paper from "@mui/material/Paper"; @@ -15,8 +17,8 @@ import * as React from "react"; import ChangeEmail from "./ChangeEmail"; import DeleteAccount from "./DeleteAccont"; import Disable2FA from "./Disable2FA"; +import DisablePasskeys from "./DisablePasskeys"; import UpdateSubscription from "./UpdateSubscription"; - export interface UserData { User: Record; Storage: Record; @@ -36,6 +38,7 @@ const UserComponent: React.FC = ({ userData }) => { const [updateSubscriptionOpen, setUpdateSubscriptionOpen] = React.useState(false); const [changeEmailOpen, setChangeEmailOpen] = React.useState(false); // State for ChangeEmail dialog + const [DisablePasskeysOpen, setDisablePasskeysOpen] = React.useState(false); React.useEffect(() => { if (userData?.Security["Two factor 2FA"] === "Enabled") { @@ -87,6 +90,20 @@ const UserComponent: React.FC = ({ userData }) => { setUpdateSubscriptionOpen(false); }; + const handleOpenDisablePasskeys = () => { + setDisablePasskeysOpen(true); // Open the CloseFamily dialog + }; + + const handleCloseDisablePasskeys = () => { + setDisablePasskeysOpen(false); // Close the CloseFamily dialog + }; + + const handleDisablePasskeys = () => { + // Implement your logic to close family here + console.log("Close family action"); + handleOpenDisablePasskeys(); // Open CloseFamily dialog after closing family + }; + if (!userData) { return null; } @@ -99,6 +116,7 @@ const UserComponent: React.FC = ({ userData }) => { component={Paper} variant="outlined" sx={{ + backgroundColor: "#F1F1F3", minHeight: 300, display: "flex", flexDirection: "column", @@ -214,6 +232,15 @@ const UserComponent: React.FC = ({ userData }) => { /> + ) : label === "Passkeys" ? ( + ) : typeof value === "string" ? ( label === "Two factor 2FA" ? ( is2FADisabled || @@ -328,6 +355,13 @@ const UserComponent: React.FC = ({ userData }) => { open={changeEmailOpen} onClose={handleCloseChangeEmail} /> + + {/* Render Passkeys Dialog */} + ); };