From 65c72f6cf5ac9efe4931ef4a418e71d32be35d94 Mon Sep 17 00:00:00 2001 From: atyabbin Date: Thu, 27 Jun 2024 15:59:39 +0530 Subject: [PATCH 1/6] Showing usage data in GB in the fetch table --- infra/staff/src/App.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infra/staff/src/App.tsx b/infra/staff/src/App.tsx index aaac53273d..9aa819e130 100644 --- a/infra/staff/src/App.tsx +++ b/infra/staff/src/App.tsx @@ -103,6 +103,8 @@ export const App: React.FC = () => { displayValue = new Date(value / 1000).toLocaleString(); } else if (key === "storage" && typeof value === "number") { displayValue = `${(value / 1024 ** 3).toFixed(2)} GB`; + } else if (key === "usage" && typeof value === "number") { + displayValue = `${(value / 1024 ** 3).toFixed(2)} GB`; } else if (typeof value === "string") { try { const parsedValue = JSON.parse( From 2e35b1eeb489231e59cde0b1897ee03a0c745509 Mon Sep 17 00:00:00 2001 From: atyabbin Date: Sat, 6 Jul 2024 16:33:26 +0530 Subject: [PATCH 2/6] First view of the dashboard with new UI --- infra/staff/package.json | 4 + infra/staff/src/App.css | 475 +++---------------------- infra/staff/src/App.tsx | 237 +++--------- infra/staff/src/components/Sidebar.tsx | 5 - infra/staff/src/components/duckie.png | Bin 0 -> 6648 bytes 5 files changed, 92 insertions(+), 629 deletions(-) create mode 100644 infra/staff/src/components/duckie.png diff --git a/infra/staff/package.json b/infra/staff/package.json index 6939e38937..a86c88e9f5 100644 --- a/infra/staff/package.json +++ b/infra/staff/package.json @@ -11,6 +11,10 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "@mui/icons-material": "^5.16.0", + "@mui/material": "^5.15.21", "date-fns": "^3.6.0", "react": "^18", "react-datepicker": "^7.1.0", diff --git a/infra/staff/src/App.css b/infra/staff/src/App.css index e080f6b3b2..8e2a1b537d 100644 --- a/infra/staff/src/App.css +++ b/infra/staff/src/App.css @@ -1,456 +1,69 @@ -.fetch-button-container { - margin-right: 180px; - margin-left: 10px; +.container { + position: relative; /* Ensure the parent is relatively positioned */ + height: 100vh; /* Full viewport height */ + width: 100vw; /* Full viewport width */ + display: flex; + justify-content: center; + align-items: center; } -.content-wrapper { +.center-table { + display: table; +} + +.input-form { display: flex; - align-items: flex-start; - margin-left: 175px; + flex-direction: column; + align-items: center; +} + +.horizontal-group { + position: absolute; /* Use absolute positioning */ + top: 32px; /* 32px below the top of the page */ + left: 32px; /* 32px from the leftmost edge of the page */ + right:32px; + display: flex; + align-items: center; /* Align items vertically centered */ + gap: 20px; /* Adjust the gap between elements as needed */ + background-color: #fafafa; + height: 104px; + width:1375px; + border-radius: 10px; } .fetch-button-container button { - padding: 10px 20px; - font-size: 16px; - cursor: pointer; - background-color: #009879; + background-color: #00b33c; color: white; border: none; - border-radius: 5px; - margin-top: 20px; -} -#submitbtn { - padding: 10px 20px; - font-size: 16px; - cursor: pointer; - background-color: #009879; - color: white; - border: none; - border-radius: 5px; - margin-top: 20px; -} - -#submitbtn:hover { - background-color: #007c6c; + width: 199px; + height: 56px; + margin-left: 20px; } .fetch-button-container button:hover { background-color: #007c6c; } -.sidebar { - padding: 20px; - flex: 0 0 auto; -} - -.button-container { +.link-text { display: flex; - gap: 10px; -} - -button { - padding: 10px 20px; - font-size: 16px; -} - -.error-message { - margin-top: 20px; - color: red; -} - -.message { - position: fixed; - bottom: 10px; - left: 50%; - transform: translateX(-50%); - padding: 10px; - border-radius: 5px; - background-color: #f0f0f0; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); -} - -.message.error { - background-color: #f44336; - color: white; -} - -.message.success { - background-color: #4caf50; - color: white; -} - -.dropdown-container { - margin-bottom: 20px; - position: relative; /* Ensure relative positioning for dropdown menu */ -} - -.more-button { - padding: 10px 20px; - font-size: 16px; - cursor: pointer; - background-color: #009879; - color: white; - border: none; - border-radius: 5px; - margin-top: 1px; -} - -.more-button:hover { - background-color: #007c6c; -} - -.dropdown-menu { - position: absolute; - top: calc(100% + 5px); /* Position right below the More button */ - left: 0; - z-index: 100; - display: block; - background-color: #fff; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - border: 1px solid #ccc; - border-radius: 5px; - padding: 10px; - max-height: 150px; /* Example: Set max height for scrollability */ - overflow-y: auto; /* Enable vertical scroll */ -} - -/* Remove bullets from unordered list */ -.dropdown-menu ul { - list-style-type: none; - padding: 0; - margin: 0; -} - -.dropdown-menu button { - display: block; - width: 100%; - padding: 8px 16px; - font-size: 14px; - color: #333; - background-color: transparent; - border: none; - cursor: pointer; - transition: background-color 0.3s ease; -} - -.dropdown-menu button:hover { - background-color: #f0f0f0; -} -.modal { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background-color: #ffffff; - padding: 20px; - border: 1px solid #ccc; - border-radius: 5px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - z-index: 1000; - max-width: 80%; - max-height: 80%; - overflow: auto; -} - -.modal-header { - display: flex; - justify-content: space-between; align-items: center; - padding-bottom: 10px; - border-bottom: 1px solid #ccc; + padding: 0 16px; /* Add padding for better appearance */ + text-decoration: none; /* Remove underline */ + color: inherit; /* Inherit color from parent */ + font-weight: bold; /* Make the text bold */ + font-size: 40px; } -.modal-header .close-btn { - cursor: pointer; - color: #777; - font-size: 20px; +.text-field-token { + margin-left: 70px !important; /* Adjust margin for Token field */ } -.modal-content { - margin-top: 10px; +.text-field-email { + margin-left: 20px !important; /* Adjust margin for Email field */ } - -/* Styles for draggable modal */ -.modal.draggable { - cursor: move; -} - -.modal.draggable .modal-header { - cursor: move; -} -.popup { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background-color: lightgreen; - padding: 20px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - z-index: 1000; -} - -.popup-content { - display: flex; - flex-direction: column; -} - -.popup-content div { - margin-bottom: 10px; -} -:root { - --popup-bg-color-light: #fff; - --popup-bg-color-dark: #2c2c2c; - --popup-border-color-light: #ccc; - --popup-border-color-dark: #444; - --popup-text-color-light: #000; - --popup-text-color-dark: #fff; - --popup-shadow-light: rgba(0, 0, 0, 0.1); - --popup-shadow-dark: rgba(255, 255, 255, 0.1); -} - -.update-subscription-popup { - position: fixed; - top: 50%; - left: 50%; - width: 400px; - transform: translate(-50%, -50%); - background-color: var(--popup-bg-color-light); - border: 1px solid var(--popup-border-color-light); - padding: 20px; - z-index: 1000; - box-shadow: 0px 0px 10px var(--popup-shadow-light); -} - -.popup-content { - display: flex; - flex-direction: column; -} - -.close-button { - align-self: flex-end; - background: none; - border: none; - font-size: 1.5rem; - cursor: pointer; - color: var(--popup-text-color-light); -} - -.popup-content h2 { - margin-top: 0; -} - -.popup-content form label { - display: block; - margin-bottom: 10px; -} - -.popup-content form input, -.popup-content form select { - width: 100%; - padding: 8px; - margin-top: 5px; - background-color: var(--popup-bg-color-light); - color: var(--popup-text-color-light); - border: 1px solid var(--popup-border-color-light); -} - -.popup-content form button { - padding: 10px 15px; - margin-top: 10px; - cursor: pointer; - background-color: var(--popup-bg-color-light); - color: var(--popup-text-color-light); - border: 1px solid var(--popup-border-color-light); -} - -.custom-select { - position: relative; - width: 100%; -} - -.custom-select select { - width: 100%; - padding: 8px; - cursor: pointer; - background-color: var(--popup-bg-color-light); - color: var(--popup-text-color-light); - border: 1px solid var(--popup-border-color-light); - appearance: none; -} - -.custom-select::after { - content: "\25BC"; +.duckie-container{ position: absolute; top: 50%; - right: 10px; - transform: translateY(-50%); - pointer-events: none; -} - -.message { - margin-top: 10px; - padding: 10px; - border-radius: 5px; -} - -.message.error { - background-color: #f8d7da; - color: #721c24; -} - -.message.success { - background-color: #d4edda; - color: #155724; -} - -@media (prefers-color-scheme: dark) { - .update-subscription-popup { - background-color: var(--popup-bg-color-dark); - border-color: var(--popup-border-color-dark); - color: var(--popup-text-color-dark); - box-shadow: 0px 0px 10px var(--popup-shadow-dark); - } - - .close-button { - color: var(--popup-text-color-dark); - } - - .popup-content form input, - .popup-content form select { - background-color: var(--popup-bg-color-dark); - color: var(--popup-text-color-dark); - border: 1px solid var(--popup-border-color-dark); - } - - .popup-content form button { - background-color: var(--popup-bg-color-dark); - color: var(--popup-text-color-dark); - border: 1px solid var(--popup-border-color-dark); - } -} -:root { - --popup-bg-color-light: #fff; - --popup-bg-color-dark: #2c2c2c; - --popup-border-color-light: #ccc; - --popup-border-color-dark: #444; - --popup-text-color-light: #000; - --popup-text-color-dark: #fff; - --popup-shadow-light: rgba(0, 0, 0, 0.1); - --popup-shadow-dark: rgba(255, 255, 255, 0.1); -} - -.update-subscription-popup { - position: fixed; - top: 50%; left: 50%; transform: translate(-50%, -50%); - background-color: var(--popup-bg-color-light); - border: 1px solid var(--popup-border-color-light); - padding: 20px; - z-index: 1000; - box-shadow: 0px 0px 10px var(--popup-shadow-light); -} - -.popup-content { - display: flex; - flex-direction: column; -} - -.close-button { - align-self: flex-end; - background: none; - border: none; - font-size: 1.5rem; - cursor: pointer; - color: var(--popup-text-color-light); -} - -.popup-content h2 { - margin-top: 0; -} - -.form-group { - margin-bottom: 20px; -} - -.form-group label { - display: block; - margin-bottom: 5px; -} - -.form-group input, -.form-group select { - width: 100%; - padding: 8px; - background-color: var(--popup-bg-color-light); - color: var(--popup-text-color-light); - border: 1px solid var(--popup-border-color-light); - border-radius: 5px; - margin-top: 5px; -} - -.custom-select { - position: relative; - width: 100%; -} - -.custom-select select { - width: 100%; - padding: 8px; - cursor: pointer; - background-color: var(--popup-bg-color-light); - color: var(--popup-text-color-light); - border: 1px solid var(--popup-border-color-light); - appearance: none; -} - -.custom-select::after { - content: "\25BC"; - position: absolute; - top: 50%; - right: 10px; - transform: translateY(-50%); - pointer-events: none; -} - -.message { - margin-top: 10px; - padding: 10px; - border-radius: 5px; -} - -.message.error { - background-color: #f8d7da; - color: #721c24; -} - -.message.success { - background-color: #d4edda; - color: #155724; -} - -@media (prefers-color-scheme: dark) { - .update-subscription-popup { - background-color: var(--popup-bg-color-dark); - border-color: var(--popup-border-color-dark); - color: var(--popup-text-color-dark); - box-shadow: 0px 0px 10px var(--popup-shadow-dark); - } - - .close-button { - color: var(--popup-text-color-dark); - } - - .form-group input, - .form-group select { - background-color: var(--popup-bg-color-dark); - color: var(--popup-text-color-dark); - border: 1px solid var(--popup-border-color-dark); - } - - .form-group button { - background-color: var(--popup-bg-color-dark); - color: var(--popup-text-color-dark); - border: 1px solid var(--popup-border-color-dark); - } -} +} \ No newline at end of file diff --git a/infra/staff/src/App.tsx b/infra/staff/src/App.tsx index 9aa819e130..0b47e29e13 100644 --- a/infra/staff/src/App.tsx +++ b/infra/staff/src/App.tsx @@ -1,9 +1,9 @@ +import Button from "@mui/material/Button"; +import TextField from "@mui/material/TextField"; import React, { useEffect, useState } from "react"; import "./App.css"; -import { Sidebar } from "./components/Sidebar"; import { apiOrigin } from "./services/support"; -import S from "./utils/strings"; - +import duckieimage from "./components/duckie.png"; type User = Record< string, string | number | boolean | null | undefined | Record @@ -13,9 +13,6 @@ type UserData = Record; export const App: React.FC = () => { const [token, setToken] = useState(""); const [email, setEmail] = useState(""); - const [userData, setUserData] = useState(null); - const [error, setError] = useState(null); - const [isDataFetched, setIsDataFetched] = useState(false); useEffect(() => { const storedToken = localStorage.getItem("token"); @@ -44,119 +41,11 @@ export const App: React.FC = () => { } const userDataResponse = (await response.json()) as UserData; console.log("API Response:", userDataResponse); - setUserData(userDataResponse); - setError(null); - setIsDataFetched(true); } catch (error) { console.error("Error fetching data:", error); - setError((error as Error).message); - setIsDataFetched(false); } }; - const renderAttributes = ( - data: Record | User | null, - ): React.ReactNode => { - if (!data) return null; - - const nullAttributes: string[] = []; - - const rows = Object.entries(data).map(([key, value]) => { - console.log("Processing key:", key, "value:", value); - - if ( - typeof value === "object" && - value !== null && - !Array.isArray(value) - ) { - return ( - - - - {key.toUpperCase()} - - - {renderAttributes( - value as Record | User, - )} - - ); - } else { - if (value === null) { - nullAttributes.push(key); - } - - let displayValue: React.ReactNode; - if (key === "expiryTime" && typeof value === "number") { - displayValue = new Date(value / 1000).toLocaleString(); - } else if ( - key === "creationTime" && - typeof value === "number" - ) { - displayValue = new Date(value / 1000).toLocaleString(); - } else if (key === "storage" && typeof value === "number") { - displayValue = `${(value / 1024 ** 3).toFixed(2)} GB`; - } else if (key === "usage" && typeof value === "number") { - displayValue = `${(value / 1024 ** 3).toFixed(2)} GB`; - } else if (typeof value === "string") { - try { - const parsedValue = JSON.parse( - value, - ) as React.ReactNode; - displayValue = parsedValue; - } catch (error) { - displayValue = value; - } - } else if (typeof value === "object" && value !== null) { - displayValue = JSON.stringify(value, null, 2); - } else if (value === null) { - displayValue = "null"; - } else if ( - typeof value === "boolean" || - typeof value === "number" - ) { - displayValue = value.toString(); - } else if (typeof value === "undefined") { - displayValue = "undefined"; - } else { - displayValue = value as string; - } - - return ( - - - {key} - - - {displayValue} - - - ); - } - }); - - console.log("Attributes with null values:", nullAttributes); - - return rows; - }; - const handleKeyPress = (event: React.KeyboardEvent) => { if (event.key === "Enter") { event.preventDefault(); @@ -168,92 +57,54 @@ export const App: React.FC = () => { return (
-

{S.hello}

-
-
); }; diff --git a/infra/staff/src/components/ChangeEmail.tsx b/infra/staff/src/components/ChangeEmail.tsx new file mode 100644 index 0000000000..68da6abb79 --- /dev/null +++ b/infra/staff/src/components/ChangeEmail.tsx @@ -0,0 +1,192 @@ +import CloseIcon from "@mui/icons-material/Close"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + TextField, +} from "@mui/material"; +import React, { useEffect, useState } from "react"; +import { getEmail, getToken } from "../App"; +import { apiOrigin } from "../services/support"; +interface ErrorResponse { + message: string; +} + +interface ChangeEmailProps { + open: boolean; + onClose: () => void; +} + +interface UserDataResponse { + subscription: { + userID: string; + } | null; +} + +const ChangeEmail: React.FC = ({ open, onClose }) => { + const [newEmail, setNewEmail] = useState(""); + const [userID, setUserID] = useState(""); + + useEffect(() => { + const fetchUserID = async () => { + const token = getToken(); + const email = getEmail(); + setNewEmail(email); // Set initial email state + + const encodedEmail = encodeURIComponent(email); + const encodedToken = encodeURIComponent(token); + const url = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`; + + try { + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-AUTH-TOKEN": token, + }, + }); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + + const data = (await response.json()) as UserDataResponse; + if (data.subscription) { + setUserID(data.subscription.userID); // Update userID state + } else { + throw new Error("Subscription data not found"); + } + } catch (error) { + console.error("Error fetching user ID:", error); + } + }; + + if (open) { + fetchUserID().catch((error: unknown) => + console.error("Error in fetchUserID:", error), + ); + } + }, [open]); + + const handleChange = (event: React.ChangeEvent) => { + setNewEmail(event.target.value); // Update newEmail state on input change + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + const token = getToken(); + const url = `${apiOrigin}/admin/user/change-email?token=${token}`; + + const body = { + userID, + email: newEmail, + }; + + try { + const response = await fetch(url, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "X-AUTH-TOKEN": token, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + let errorData; + try { + errorData = (await response.json()) as ErrorResponse; + } catch (error) { + console.error("Error parsing error response:", error); + } + throw new Error( + errorData?.message ?? "Network response was not ok", + ); + } + + console.log("Email updated successfully"); + onClose(); + } catch (error) { + console.error("Error updating email:", error); + } + }; + const handleSubmitSync: React.FormEventHandler = ( + event, + ) => { + handleSubmit(event).catch((error: unknown) => { + console.error("Error in handleSubmit:", error); + }); + }; + + return ( + + + Change Email + + + +
+
+ + +
+ + + + +
+
+
+ ); +}; + +export default ChangeEmail; diff --git a/infra/staff/src/components/DeleteAccont.tsx b/infra/staff/src/components/DeleteAccont.tsx new file mode 100644 index 0000000000..84c9c47fa2 --- /dev/null +++ b/infra/staff/src/components/DeleteAccont.tsx @@ -0,0 +1,100 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Paper, +} from "@mui/material"; +import React from "react"; +import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions +import { apiOrigin } from "../services/support"; + +interface DeleteAccountProps { + open: boolean; + handleClose: () => void; +} + +const DeleteAccount: React.FC = ({ open, handleClose }) => { + const handleDelete = async () => { + try { + const encodedEmail = encodeURIComponent(getEmail()); + console.log(encodedEmail); + const encodedToken = encodeURIComponent(getToken()); + console.log(encodedToken); + const deleteUrl = `${apiOrigin}/admin/user/delete?email=${encodedEmail}&token=${encodedToken}`; + const response = await fetch(deleteUrl, { method: "DELETE" }); + if (!response.ok) { + throw new Error("Failed to delete user account"); + } + handleClose(); // Close dialog on successful delete + console.log("Account deleted successfully"); + } catch (error) { + console.error("Error deleting user account:", error); + } + }; + + return ( +
+ + + {"Delete Account?"} + + + + Are you sure you want to delete the account? + + + + + + + +
+ ); +}; + +export default DeleteAccount; diff --git a/infra/staff/src/components/Disable2FA.tsx b/infra/staff/src/components/Disable2FA.tsx new file mode 100644 index 0000000000..c43dbd0992 --- /dev/null +++ b/infra/staff/src/components/Disable2FA.tsx @@ -0,0 +1,156 @@ +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 Disable2FAProps { + open: boolean; + handleClose: () => void; + handleDisable2FA: () => void; // Callback to handle 2FA disablement +} + +const Disable2FA: React.FC = ({ + open, + handleClose, + handleDisable2FA, +}) => { + const [loading, setLoading] = useState(false); + + const handleDisable = 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 2FA + const disableUrl = `${apiOrigin}/admin/user/disable-2fa?token=${encodedToken}`; + const body = JSON.stringify({ userId }); + const disableResponse = await fetch(disableUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: body, + }); + + if (!disableResponse.ok) { + const errorResponse = await disableResponse.text(); + throw new Error(`Failed to disable 2FA: ${errorResponse}`); + } + + handleDisable2FA(); // Notify parent component of successful disable + handleClose(); // Close dialog on successful disable + console.log("2FA disabled successfully"); + } catch (error) { + console.error("Error disabling 2FA:", error); + } finally { + setLoading(false); + } + }; + + const handleCancel = () => { + handleClose(); // Close dialog + }; + + return ( +
+ + + {"Disable 2FA?"} + + + + Are you sure you want to disable 2FA for this account? + + + + + + + +
+ ); +}; + +export default Disable2FA; diff --git a/infra/staff/src/components/UpdateSubscription.tsx b/infra/staff/src/components/UpdateSubscription.tsx index 6e415d4134..f1b36e25c4 100644 --- a/infra/staff/src/components/UpdateSubscription.tsx +++ b/infra/staff/src/components/UpdateSubscription.tsx @@ -1,178 +1,366 @@ +import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; +import CloseIcon from "@mui/icons-material/Close"; +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogTitle from "@mui/material/DialogTitle"; +import Grid from "@mui/material/Grid"; +import InputAdornment from "@mui/material/InputAdornment"; +import MenuItem from "@mui/material/MenuItem"; +import Select, { type SelectChangeEvent } from "@mui/material/Select"; +import TextField from "@mui/material/TextField"; import React, { useEffect, useState } from "react"; import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; -import "../App.css"; +import { getEmail, getToken } from "../App"; import { apiOrigin } from "../services/support"; +interface Subscription { + productID: string; + paymentProvider: string; + storage: number; + originalTransactionID: string; + expiryTime: number; + userID: string; +} + +interface UserDataResponse { + subscription: Subscription | null; +} + interface UpdateSubscriptionProps { - token: string; - userId: string; + open: boolean; onClose: () => void; } -export const UpdateSubscription: React.FC = ({ - token, - userId, +interface FormValues { + productId: string; + provider: string; + storage: number; + transactionId: string; + expiryTime: string | Date | null; + userId: string; +} + +const UpdateSubscription: React.FC = ({ + open, onClose, }) => { - const [expiryTime, setExpiryTime] = useState(null); - const [productId, setProductId] = useState("50gb_monthly"); - const [paymentProvider, setPaymentProvider] = useState("bitpay"); - const [transactionId, setTransactionId] = useState(""); - const [message, setMessage] = useState(null); - const [error, setError] = useState(null); - const [storage, setStorage] = useState(""); + const [values, setValues] = useState({ + productId: "", + provider: "", + storage: 0, + transactionId: "", + expiryTime: "", + userId: "", + }); + + const [isDatePickerOpen, setIsDatePickerOpen] = useState(false); useEffect(() => { - if (productId === "50gb_yearly" || productId === "50gb_monthly") { - setStorage(50 * 1024 * 1024 * 1024); - } else if ( - productId === "200gb_yearly" || - productId === "200gb_monthly" - ) { - setStorage(200 * 1024 * 1024 * 1024); - } else if ( - productId === "500gb_yearly" || - productId === "500gb_monthly" - ) { - setStorage(500 * 1024 * 1024 * 1024); - } else if ( - productId === "2000gb_yearly" || - productId === "2000gb_monthly" - ) { - setStorage(2000 * 1024 * 1024 * 1024); - } else { - setStorage(""); - } - }, [productId]); + const fetchData = async () => { + try { + const email = getEmail(); + const token = getToken(); + const encodedEmail = encodeURIComponent(email); + const encodedToken = encodeURIComponent(token); + const url = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`; - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); + const response = await fetch(url); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + const userDataResponse = + (await response.json()) as UserDataResponse; - const expiryTimeTimestamp = expiryTime - ? expiryTime.getTime() * 1000 - : ""; + if (!userDataResponse.subscription) { + throw new Error("Subscription data not found"); + } - const url = `${apiOrigin}/admin/user/subscription`; - const body = { - userId, - storage, - expiryTime: expiryTimeTimestamp, - productId, - paymentProvider, - transactionId, + const expiryTime = new Date( + userDataResponse.subscription.expiryTime / 1000, + ); + + setValues({ + productId: userDataResponse.subscription.productID || "", + provider: + userDataResponse.subscription.paymentProvider || "", + storage: + userDataResponse.subscription.storage / + (1024 * 1024 * 1024) || 0, + transactionId: + userDataResponse.subscription.originalTransactionID || + "", + expiryTime: expiryTime, + userId: userDataResponse.subscription.userID || "", + }); + } catch (error) { + console.error("Error fetching data:", error); + } }; - try { - const response = await fetch(url, { - method: "PUT", - headers: { - "Content-Type": "application/json", - "X-AUTH-TOKEN": token, - }, - body: JSON.stringify(body), + fetchData().catch((error: unknown) => { + console.error("Unhandled promise rejection:", error); + }); + }, []); + + const handleCalendarClick = () => { + setIsDatePickerOpen(true); + }; + + const handleChange = (event: React.ChangeEvent) => { + const { name, value } = event.target; + setValues({ + ...values, + [name]: name === "storage" ? parseInt(value, 10) : value, + }); + }; + + const handleChangeProvider = (event: SelectChangeEvent) => { + const { name, value } = event.target; + + if (name) { + setValues({ + ...values, + [name]: value, }); - - if (!response.ok) { - throw new Error( - `Network response was not ok: ${response.status}`, - ); - } - - setMessage("Subscription updated successfully"); - setError(null); - setTimeout(() => { - setMessage(null); - onClose(); - }, 1000); - } catch (error) { - console.error("Error updating subscription:", error); - setError( - error instanceof Error && typeof error.message === "string" - ? error.message - : "An unexpected error occurred", - ); - setTimeout(() => { - setError(null); - }, 1000); } }; - const handleSubmitWrapper = (event: React.FormEvent) => { - handleSubmit(event).catch((error: unknown) => { - console.error("Error in handleSubmit:", error); + const handleDatePickerChange = (date: Date | null) => { + setValues({ + ...values, + expiryTime: date, + }); + setIsDatePickerOpen(false); + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + (async () => { + const token = getToken(); + const url = `${apiOrigin}/admin/user/subscription`; + + let expiryTime = null; + if (values.expiryTime instanceof Date) { + const utcExpiryTime = new Date(values.expiryTime); + expiryTime = utcExpiryTime.getTime() * 1000; + } + + const body = { + userId: values.userId, + storage: values.storage * (1024 * 1024 * 1024), + expiryTime: expiryTime, + productId: values.productId, + paymentProvider: values.provider, + transactionId: values.transactionId, + }; + + try { + const response = await fetch(url, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "X-AUTH-TOKEN": token, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + console.log("Subscription updated successfully"); + onClose(); + } catch (error) { + console.error("Error updating subscription:", error); + } + })().catch((error: unknown) => { + console.error("Unhandled promise rejection:", error); }); }; return ( -
-
- -

Update Subscription

-
-
- - setExpiryTime(date)} - dateFormat="dd/MM/yyyy" - showYearDropdown - scrollableYearDropdown - yearDropdownItemNumber={15} - /> -
-
- - + Stripe + PayPal + BitPay + +
+ + +
+ + +
+
+ +
+ + +
+
+ +
+ + + + + ), + readOnly: true, + }} + fullWidth + /> + {isDatePickerOpen && ( + + setIsDatePickerOpen(false) + } + withPortal + inline + /> + )} +
+
+ + +
-
- - -
-
- - setTransactionId(e.target.value)} - /> -
- + Update + + - {(error ?? message) && ( -
- {error ? `Error: ${error}` : `Success: ${message}`} -
- )} -
-
+ + ); }; diff --git a/infra/staff/src/components/UserComponent.tsx b/infra/staff/src/components/UserComponent.tsx new file mode 100644 index 0000000000..645572162b --- /dev/null +++ b/infra/staff/src/components/UserComponent.tsx @@ -0,0 +1,335 @@ +import DeleteIcon from "@mui/icons-material/Delete"; +import EditIcon from "@mui/icons-material/Edit"; +import Box from "@mui/material/Box"; +import Grid from "@mui/material/Grid"; +import IconButton from "@mui/material/IconButton"; +import Paper from "@mui/material/Paper"; +import Switch from "@mui/material/Switch"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableRow from "@mui/material/TableRow"; +import Typography from "@mui/material/Typography"; +import * as React from "react"; +import ChangeEmail from "./ChangeEmail"; +import DeleteAccount from "./DeleteAccont"; +import Disable2FA from "./Disable2FA"; +import UpdateSubscription from "./UpdateSubscription"; + +export interface UserData { + User: Record; + Storage: Record; + Subscription: Record; + Security: Record; +} + +interface UserComponentProps { + userData: UserData | null; +} + +const UserComponent: React.FC = ({ userData }) => { + const [deleteAccountOpen, setDeleteAccountOpen] = React.useState(false); + const [disable2FAOpen, setDisable2FAOpen] = React.useState(false); + const [twoFactorEnabled, setTwoFactorEnabled] = React.useState(false); + const [is2FADisabled, setIs2FADisabled] = React.useState(false); + const [updateSubscriptionOpen, setUpdateSubscriptionOpen] = + React.useState(false); + const [changeEmailOpen, setChangeEmailOpen] = React.useState(false); // State for ChangeEmail dialog + + React.useEffect(() => { + if (userData?.Security["Two factor 2FA"] === "Enabled") { + setTwoFactorEnabled(true); + } else { + setTwoFactorEnabled(false); + } + }, [userData]); + + const handleEditEmail = () => { + console.log("Edit Email clicked"); + setChangeEmailOpen(true); + }; + + const handleCloseChangeEmail = () => { + setChangeEmailOpen(false); // Close ChangeEmail dialog + }; + + const handleDeleteAccountClick = () => { + setDeleteAccountOpen(true); + }; + + const handleCloseDeleteAccount = () => { + setDeleteAccountOpen(false); + }; + + const handleOpenDisable2FA = () => { + setDisable2FAOpen(true); + }; + + const handleCloseDisable2FA = () => { + setDisable2FAOpen(false); + }; + + const handleDisable2FA = () => { + setIs2FADisabled(true); + }; + + const handleCancelDisable2FA = () => { + setTwoFactorEnabled(true); + handleCloseDisable2FA(); + }; + + const handleEditSubscription = () => { + setUpdateSubscriptionOpen(true); + }; + + const handleCloseUpdateSubscription = () => { + setUpdateSubscriptionOpen(false); + }; + + if (!userData) { + return null; + } + + return ( + + {Object.entries(userData).map(([title, data]) => ( + + + + + {title} + + {title === "User" && ( + + + + )} + {title === "Subscription" && ( + + + + )} + + + + + {Object.entries( + data as Record, + ).map(([label, value], index) => ( + + + {label} + + + {label === "Email" ? ( + + + {value} + + + + + + ) : typeof value === "string" ? ( + label === "Two factor 2FA" ? ( + is2FADisabled || + value === "Disabled" ? ( + + {value} + + ) : ( + + + {value} + + {value === + "Enabled" && ( + { + const isChecked = + e + .target + .checked; + setTwoFactorEnabled( + isChecked, + ); + if ( + !isChecked + ) { + handleOpenDisable2FA(); + } + }} + sx={{ + "& .MuiSwitch-switchBase.Mui-checked": + { + color: "#00B33C", + "&:hover": + { + backgroundColor: + "rgba(0, 179, 60, 0.08)", + }, + }, + "& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track": + { + backgroundColor: + "#00B33C", + }, + }} + /> + )} + + ) + ) : ( + + {value} + + ) + ) : ( + + {String(value)} + + )} + + + ))} + +
+
+
+ ))} + + {/* Render DeleteAccount dialog */} + + + {/* Render Disable2FA dialog */} + + + {/* Render UpdateSubscription dialog */} + + + {/* Render ChangeEmail dialog */} + +
+ ); +}; + +export default UserComponent; diff --git a/infra/staff/src/main.tsx b/infra/staff/src/main.tsx index 528729a238..162cfad3cf 100644 --- a/infra/staff/src/main.tsx +++ b/infra/staff/src/main.tsx @@ -1,6 +1,6 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import { App } from "./App"; +import App from "./App"; import "./styles/globals.css"; const root = document.getElementById("root"); From bd3e0c92890d96668a2e75a7268ba6e1ae01677e Mon Sep 17 00:00:00 2001 From: atyabbin Date: Tue, 16 Jul 2024 17:15:58 +0530 Subject: [PATCH 4/6] Deleted Sidbar.tsx --- infra/staff/src/components/Sidebar.tsx | 251 ------------------------- 1 file changed, 251 deletions(-) delete mode 100644 infra/staff/src/components/Sidebar.tsx diff --git a/infra/staff/src/components/Sidebar.tsx b/infra/staff/src/components/Sidebar.tsx deleted file mode 100644 index 35d977f414..0000000000 --- a/infra/staff/src/components/Sidebar.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import "../App.css"; -import { apiOrigin } from "../services/support"; -import UpdateSubscription from "./UpdateSubscription"; // Import the UpdateSubscription component - -interface UserData { - user: { - ID: string; - }; -} -interface ActionResponse { - success?: boolean; - message?: string; -} - -export const Sidebar: React.FC = ({ token, email }) => { - const [userId, setUserId] = useState(null); - const [error, setError] = useState(null); - const [message, setMessage] = useState(null); - const [dropdownVisible, setDropdownVisible] = useState(false); - const [showUpdateSubscription, setShowUpdateSubscription] = - useState(false); // State to control UpdateSubscription popup - - const dropdownRef = useRef(null); - - useEffect(() => { - document.addEventListener("mousedown", handleClickOutside); - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, []); - - const handleClickOutside = (event: MouseEvent) => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) - ) { - setDropdownVisible(false); - } - }; - - const fetchData = async (): Promise => { - if (!email || !token) { - setError("Email or token is missing."); - return null; - } - - try { - const url = `${apiOrigin}/admin/user?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`; - const response = await fetch(url); - if (!response.ok) { - throw new Error("Network response was not ok"); - } - const userDataResponse = (await response.json()) as UserData; - const fetchedUserId = userDataResponse.user.ID; - if (!fetchedUserId) { - throw new Error("User ID not found in response"); - } - setUserId(fetchedUserId); - setError(null); - return fetchedUserId; - } catch (error) { - console.error("Error fetching data:", error); - setError( - error instanceof Error && typeof error.message === "string" - ? error.message - : "An unexpected error occurred", - ); - - setTimeout(() => { - setError(null); - }, 1000); - return null; - } - }; - - const performAction = async (userId: string, action: string) => { - try { - const actionUrls: Record = { - Disable2FA: "/admin/user/disable-2fa", - DisablePasskeys: "/admin/user/disable-passkeys", - Closefamily: "/admin/user/close-family", - }; - - const url = `${apiOrigin}${actionUrls[action]}?id=${encodeURIComponent(userId)}&token=${encodeURIComponent(token)}`; - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ userId }), - }); - - if (!response.ok) { - throw new Error( - `Network response was not ok: ${response.status}`, - ); - } - - const result = (await response.json()) as ActionResponse; - console.log("API Response:", result); - - setMessage(`${action} completed successfully`); - setError(null); - setTimeout(() => { - setMessage(null); - }, 1000); - setDropdownVisible(false); - } catch (error) { - console.error(`Error ${action}:`, error); - setError( - error instanceof Error && typeof error.message === "string" - ? error.message - : "An unexpected error occurred", - ); - - setTimeout(() => { - setError(null); - }, 1000); - setMessage(null); - } - }; - - const deleteUser = async () => { - try { - const url = `${apiOrigin}/admin/user/delete?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`; - const response = await fetch(url, { - method: "DELETE", - }); - - if (!response.ok) { - throw new Error( - `Network response was not ok: ${response.status}`, - ); - } - - setMessage("Delete Account completed successfully"); - setError(null); - setTimeout(() => { - setMessage(null); - }, 1000); - setDropdownVisible(false); - } catch (error) { - console.error(`Error deleting account:`, error); - setError( - error instanceof Error && typeof error.message === "string" - ? error.message - : "An unexpected error occurred", - ); - - setTimeout(() => { - setError(null); - }, 1000); - setMessage(null); - } - }; - - const handleActionClick = async (action: string) => { - try { - if (action === "UpdateSubscription") { - const fetchedUserId = await fetchData(); - if (fetchedUserId) { - setShowUpdateSubscription(true); - } - return; - } - - if (action === "DeleteAccount") { - await deleteUser(); - return; - } - - const fetchedUserId = await fetchData(); - if (!fetchedUserId) { - throw new Error("Incorrect email id or token"); - } - - await performAction(fetchedUserId, action); - } catch (error) { - console.error(`Error performing ${action}:`, error); - setError( - error instanceof Error && typeof error.message === "string" - ? error.message - : "An unexpected error occurred", - ); - - setTimeout(() => { - setError(null); - }, 1000); - setMessage(null); - } - }; - - const toggleDropdown = () => { - setDropdownVisible(!dropdownVisible); - }; - - const dropdownOptions = [ - { value: "Disable2FA", label: "Disable 2FA" }, - { value: "Closefamily", label: "Close Family" }, - { value: "DisablePasskeys", label: "Disable Passkeys" }, - { value: "DeleteAccount", label: "Delete Account" }, - { value: "UpdateSubscription", label: "Update Subscription" }, // New option added here - ]; - - return ( -
-
- - {dropdownVisible && ( -
-
    - {dropdownOptions.map((option) => ( -
  • - -
  • - ))} -
-
- )} -
- {(error ?? message) && ( -
- {error ? `Error: ${error}` : `Success: ${message}`} -
- )} - {showUpdateSubscription && userId && ( - setShowUpdateSubscription(false)} - /> - )} -
- ); -}; - -export default Sidebar; From 0c6d27c1347d0d2da9ed1615f0b593548de9f127 Mon Sep 17 00:00:00 2001 From: atyabbin Date: Thu, 18 Jul 2024 17:58:42 +0530 Subject: [PATCH 5/6] Full dashboard --- infra/staff/src/App.css | 41 ++++ infra/staff/src/App.tsx | 76 +++++--- infra/staff/src/components/CloseFamily.tsx | 157 ++++++++++++++++ .../staff/src/components/DisablePasskeys.tsx | 157 ++++++++++++++++ .../src/components/FamilyComponentTable.tsx | 176 ++++++++++++++++++ .../components/StorageBonusTableComponent.tsx | 160 ++++++++++++++++ .../src/components/UpdateSubscription.tsx | 1 + infra/staff/src/components/UserComponent.tsx | 36 +++- 8 files changed, 775 insertions(+), 29 deletions(-) create mode 100644 infra/staff/src/components/CloseFamily.tsx create mode 100644 infra/staff/src/components/DisablePasskeys.tsx create mode 100644 infra/staff/src/components/FamilyComponentTable.tsx create mode 100644 infra/staff/src/components/StorageBonusTableComponent.tsx 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 */} + ); }; From 371a1805f02e843bb4e74eb43ea1ea21b1d7cd9a Mon Sep 17 00:00:00 2001 From: atyabbin Date: Thu, 18 Jul 2024 18:42:40 +0530 Subject: [PATCH 6/6] Updated some new components --- infra/staff/src/App.tsx | 2 +- infra/staff/src/components/StorageBonusTableComponent.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/staff/src/App.tsx b/infra/staff/src/App.tsx index b87f401271..1db3e87212 100644 --- a/infra/staff/src/App.tsx +++ b/infra/staff/src/App.tsx @@ -7,8 +7,8 @@ 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 StorageBonusTableComponent from "./components/StorageBonusTableComponent"; import type { UserData } from "./components/UserComponent"; import UserComponent from "./components/UserComponent"; import duckieimage from "./components/duckie.png"; diff --git a/infra/staff/src/components/StorageBonusTableComponent.tsx b/infra/staff/src/components/StorageBonusTableComponent.tsx index 3b67c3ee2b..e914358f5a 100644 --- a/infra/staff/src/components/StorageBonusTableComponent.tsx +++ b/infra/staff/src/components/StorageBonusTableComponent.tsx @@ -96,7 +96,7 @@ const StorageBonusTableComponent: React.FC = () => {