Compare commits
19 Commits
cli-v0.1.1
...
improvedas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da0832fc7d | ||
|
|
827ac9ddf7 | ||
|
|
2e35b1eeb4 | ||
|
|
65c72f6cf5 | ||
|
|
f11cc82e44 | ||
|
|
206e387834 | ||
|
|
567dfb7e6b | ||
|
|
b7d3e5439a | ||
|
|
ef37a4cad8 | ||
|
|
480a86af0a | ||
|
|
a5b0bc259d | ||
|
|
326e673d12 | ||
|
|
00b68131a8 | ||
|
|
de8d81300f | ||
|
|
e287e80257 | ||
|
|
d716f18c2e | ||
|
|
93bddbe6f1 | ||
|
|
17e48ed83f | ||
|
|
4c7583240f |
5623
infra/staff/package-lock.json
generated
Normal file
5623
infra/staff/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,11 +11,23 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@date-io/date-fns": "^3.0.0",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@mui/icons-material": "^5.16.0",
|
||||
"@mui/lab": "^5.0.0-alpha.171",
|
||||
"@mui/material": "^5.16.0",
|
||||
"@mui/x-date-pickers": "^7.9.0",
|
||||
"@types/react-datepicker": "^6.2.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"react": "^18",
|
||||
"react-datepicker": "^7.3.0",
|
||||
"react-dom": "^18",
|
||||
"react-toastify": "^10.0.5",
|
||||
"zod": "^3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@typescript-eslint/eslint-plugin": "^7",
|
||||
|
||||
306
infra/staff/src/App.css
Normal file
306
infra/staff/src/App.css
Normal file
@@ -0,0 +1,306 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.center-table {
|
||||
display: table;
|
||||
}
|
||||
|
||||
.input-form {
|
||||
display: flex;
|
||||
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 {
|
||||
background-color: #00b33c;
|
||||
color: white;
|
||||
border: none;
|
||||
width: 199px;
|
||||
height: 56px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.fetch-button-container button:hover {
|
||||
background-color: #007c6c;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
|
||||
.text-field-token {
|
||||
margin-left: 70px !important; /* Adjust margin for Token field */
|
||||
}
|
||||
|
||||
.text-field-email {
|
||||
margin-left: 20px !important; /* Adjust margin for Email field */
|
||||
}
|
||||
.duckie-container {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.center-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh; /* Adjust as necessary */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: red;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.duckie-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.container {
|
||||
position: relative; /* Ensure the parent is relatively positioned */
|
||||
height: 100vh; /* Full viewport height */
|
||||
width: 100vw; /* Full viewport width */
|
||||
display: flex;
|
||||
flex-direction: column; /* Add this */
|
||||
justify-content: center; /* Center vertically */
|
||||
align-items: center; /* Center horizontally */
|
||||
}
|
||||
|
||||
.input-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 1; /* Ensure form is on top */
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px; /* Adjust the gap between elements as needed */
|
||||
background-color: #fafafa;
|
||||
padding: 20px; /* Add padding around content */
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.fetch-button-container button {
|
||||
background-color: #00b33c;
|
||||
color: white;
|
||||
border: none;
|
||||
width: 199px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.fetch-button-container button:hover {
|
||||
background-color: #007c6c;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
|
||||
.text-field-token {
|
||||
margin-left: 70px !important; /* Adjust margin for Token field */
|
||||
}
|
||||
|
||||
.text-field-email {
|
||||
margin-left: 50px !important; /* Adjust margin for Email field */
|
||||
}
|
||||
|
||||
.center-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.duckie-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Example CSS to maintain tabs fixed position */
|
||||
.tabs-container {
|
||||
position: sticky;
|
||||
top: 20px; /* Adjust as needed */
|
||||
z-index: 1000; /* Ensure tabs are above other content */
|
||||
}
|
||||
.container {
|
||||
position: relative; /* Ensure the parent is relatively positioned */
|
||||
height: 100vh; /* Full viewport height */
|
||||
width: 100vw; /* Full viewport width */
|
||||
display: flex;
|
||||
flex-direction: column; /* Ensure children stack vertically */
|
||||
justify-content: center; /* Center vertically */
|
||||
align-items: center; /* Center horizontally */
|
||||
}
|
||||
|
||||
.input-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 1; /* Ensure form is on top */
|
||||
margin-bottom: 20px; /* Add space between form and tabs */
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px; /* Adjust the gap between elements as needed */
|
||||
background-color: #fafafa;
|
||||
padding: 20px; /* Add padding around content */
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.fetch-button-container button {
|
||||
background-color: #00b33c;
|
||||
color: white;
|
||||
border: none;
|
||||
width: 199px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.fetch-button-container button:hover {
|
||||
background-color: #007c6c;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
|
||||
.text-field-token {
|
||||
margin-left: 70px !important; /* Adjust margin for Token field */
|
||||
}
|
||||
|
||||
.text-field-email {
|
||||
margin-left: 50px !important; /* Adjust margin for Email field */
|
||||
}
|
||||
|
||||
.center-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.duckie-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
position: sticky; /* Make the tabs sticky */
|
||||
top: 0; /* Stick to the top of the viewport */
|
||||
z-index: 1000; /* Ensure tabs are above other content */
|
||||
background-color: #fafafa; /* Optional: Add background color to tabs */
|
||||
padding: 10px 20px; /* Optional: Add padding for better appearance */
|
||||
border-bottom: 1px solid #ccc; /* Optional: Add bottom border */
|
||||
}
|
||||
.dialog-popup-container {
|
||||
/* Styles for the overlay/background */
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dialog-popup {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dialog-popup-header h2 {
|
||||
margin-bottom: 20px; /* Add space between heading and fields */
|
||||
}
|
||||
|
||||
.dialog-popup-field {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.dialog-popup-field label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dialog-popup-field input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: #4caf50; /* Green */
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Specific styles for each field to match the image */
|
||||
.name-field input,
|
||||
.shortname-field input,
|
||||
.code-field input {
|
||||
width: calc(100% - 100px); /* Adjust as needed */
|
||||
}
|
||||
|
||||
.active-field input {
|
||||
width: 50px;
|
||||
}
|
||||
@@ -1,224 +1,323 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import "./App.css";
|
||||
import type { UserData } from "./components/UserComponent";
|
||||
import UserComponent from "./components/UserComponent";
|
||||
import duckieimage from "./components/duckie.png";
|
||||
import { apiOrigin } from "./services/support";
|
||||
import S from "./utils/strings";
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const [token, setToken] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [userData, setUserData] = useState<any>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Define and export email and token variables and their setter functions
|
||||
export let email = "";
|
||||
export let token = "";
|
||||
|
||||
export const setEmail = (newEmail: string) => {
|
||||
email = newEmail;
|
||||
};
|
||||
|
||||
export const setToken = (newToken: string) => {
|
||||
token = newToken;
|
||||
};
|
||||
|
||||
export const getEmail = () => email;
|
||||
export const getToken = () => token;
|
||||
|
||||
interface User {
|
||||
ID: string;
|
||||
email: string;
|
||||
creationTime: number;
|
||||
}
|
||||
|
||||
interface Subscription {
|
||||
productID: string;
|
||||
paymentProvider: string;
|
||||
expiryTime: number;
|
||||
storage: number;
|
||||
}
|
||||
|
||||
interface Security {
|
||||
isEmailMFAEnabled: boolean;
|
||||
isTwoFactorEnabled: boolean;
|
||||
passkeys: string; // Replace with actual passkey value if available
|
||||
}
|
||||
|
||||
interface UserResponse {
|
||||
user: User;
|
||||
subscription: Subscription;
|
||||
details?: {
|
||||
usage?: number;
|
||||
storageBonus?: number;
|
||||
profileData: Security;
|
||||
};
|
||||
}
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [localEmail, setLocalEmail] = useState<string>(getEmail());
|
||||
const [localToken, setLocalToken] = useState<string>(getToken());
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>("");
|
||||
const [fetchSuccess, setFetchSuccess] = useState<boolean>(false);
|
||||
const [tabValue, setTabValue] = useState<number>(0);
|
||||
const [userData, setUserData] = useState<UserData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const storedToken = localStorage.getItem("token");
|
||||
if (storedToken) {
|
||||
setToken(storedToken);
|
||||
setLocalToken(storedToken);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
localStorage.setItem("token", token);
|
||||
if (localToken) {
|
||||
setToken(localToken);
|
||||
localStorage.setItem("token", localToken);
|
||||
} else {
|
||||
localStorage.removeItem("token");
|
||||
}
|
||||
}, [token]);
|
||||
}, [localToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (localEmail) {
|
||||
setEmail(localEmail);
|
||||
localStorage.setItem("email", localEmail);
|
||||
} else {
|
||||
localStorage.removeItem("email");
|
||||
}
|
||||
}, [localEmail]);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
setFetchSuccess(false);
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const url = `${apiOrigin}/admin/user?email=${email}&token=${token}`;
|
||||
const encodedEmail = encodeURIComponent(localEmail);
|
||||
const encodedToken = encodeURIComponent(localToken);
|
||||
const url = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`;
|
||||
console.log(`Fetching data from URL: ${url}`);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
const userData = await response.json();
|
||||
console.log("API Response:", userData);
|
||||
setUserData(userData);
|
||||
setError(null);
|
||||
const userDataResponse: UserResponse =
|
||||
(await response.json()) as UserResponse;
|
||||
console.log("API Response:", userDataResponse);
|
||||
|
||||
const extractedUserData: UserData = {
|
||||
User: {
|
||||
"User ID": userDataResponse.user.ID || "None",
|
||||
Email: userDataResponse.user.email || "None",
|
||||
"Creation time":
|
||||
new Date(
|
||||
userDataResponse.user.creationTime / 1000,
|
||||
).toLocaleString() || "None",
|
||||
},
|
||||
Storage: {
|
||||
Total: userDataResponse.subscription.storage
|
||||
? userDataResponse.subscription.storage >= 1024 ** 3
|
||||
? `${(userDataResponse.subscription.storage / 1024 ** 3).toFixed(2)} GB`
|
||||
: `${(userDataResponse.subscription.storage / 1024 ** 2).toFixed(2)} MB`
|
||||
: "None",
|
||||
Consumed:
|
||||
userDataResponse.details?.usage !== undefined
|
||||
? userDataResponse.details.usage >= 1024 ** 3
|
||||
? `${(userDataResponse.details.usage / 1024 ** 3).toFixed(2)} GB`
|
||||
: `${(userDataResponse.details.usage / 1024 ** 2).toFixed(2)} MB`
|
||||
: "None",
|
||||
Bonus:
|
||||
userDataResponse.details?.storageBonus !== undefined
|
||||
? userDataResponse.details.storageBonus >= 1024 ** 3
|
||||
? `${(userDataResponse.details.storageBonus / 1024 ** 3).toFixed(2)} GB`
|
||||
: `${(userDataResponse.details.storageBonus / 1024 ** 2).toFixed(2)} MB`
|
||||
: "None",
|
||||
},
|
||||
Subscription: {
|
||||
"Product ID":
|
||||
userDataResponse.subscription.productID || "None",
|
||||
Provider:
|
||||
userDataResponse.subscription.paymentProvider || "None",
|
||||
"Expiry time":
|
||||
new Date(
|
||||
userDataResponse.subscription.expiryTime / 1000,
|
||||
).toLocaleString() || "None",
|
||||
},
|
||||
Security: {
|
||||
"Email MFA": userDataResponse.details?.profileData
|
||||
.isEmailMFAEnabled
|
||||
? "Enabled"
|
||||
: "Disabled",
|
||||
"Two factor 2FA": userDataResponse.details?.profileData
|
||||
.isTwoFactorEnabled
|
||||
? "Enabled"
|
||||
: "Disabled",
|
||||
Passkeys: "None", // Replace with actual passkey value if available
|
||||
},
|
||||
};
|
||||
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
const delay = Math.max(3000 - elapsedTime, 0);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
setFetchSuccess(true);
|
||||
setUserData(extractedUserData);
|
||||
}, delay);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
setError((error as Error).message);
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
const delay = Math.max(3000 - elapsedTime, 0);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
setError("Invalid token or email id");
|
||||
}, delay);
|
||||
}
|
||||
};
|
||||
|
||||
const renderAttributes = (data: any) => {
|
||||
if (!data) return null;
|
||||
const handleKeyPress = (event: React.KeyboardEvent<HTMLFormElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
fetchData().catch((error: unknown) =>
|
||||
console.error("Fetch data error:", error),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let 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 (
|
||||
<React.Fragment key={key}>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={2}
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
backgroundColor: "#f1f1f1",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
{key.toUpperCase()}
|
||||
</td>
|
||||
</tr>
|
||||
{renderAttributes(value)}
|
||||
</React.Fragment>
|
||||
);
|
||||
} 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 (typeof value === "string") {
|
||||
try {
|
||||
const parsedValue = JSON.parse(value);
|
||||
displayValue = parsedValue;
|
||||
} catch (error) {
|
||||
displayValue = value;
|
||||
}
|
||||
} else if (value === null) {
|
||||
displayValue = "null";
|
||||
} else if (typeof value !== "undefined") {
|
||||
displayValue = value.toString();
|
||||
} else {
|
||||
displayValue = "undefined";
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td
|
||||
style={{
|
||||
padding: "10px",
|
||||
border: "1px solid #ddd",
|
||||
}}
|
||||
>
|
||||
{key}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: "10px",
|
||||
border: "1px solid #ddd",
|
||||
}}
|
||||
>
|
||||
{displayValue}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Attributes with null values:", nullAttributes);
|
||||
|
||||
return rows;
|
||||
const handleTabChange = (
|
||||
_event: React.SyntheticEvent,
|
||||
newValue: number,
|
||||
) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container center-table">
|
||||
<h1>{S.hello}</h1>
|
||||
<form className="input-form" onKeyPress={handleKeyPress}>
|
||||
<div className="horizontal-group">
|
||||
<a
|
||||
href="https://staff.ente.sh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link-text"
|
||||
>
|
||||
staff.ente.sh
|
||||
</a>
|
||||
|
||||
<form className="input-form">
|
||||
<div className="input-group">
|
||||
<label>
|
||||
Token:
|
||||
<input
|
||||
type="text"
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
style={{
|
||||
padding: "10px",
|
||||
margin: "10px",
|
||||
width: "100%",
|
||||
<TextField
|
||||
label="Token"
|
||||
value={localToken}
|
||||
onChange={(e) => {
|
||||
setLocalToken(e.target.value);
|
||||
setToken(e.target.value);
|
||||
}}
|
||||
size="medium"
|
||||
className="text-field-token"
|
||||
style={{ width: "350px" }}
|
||||
/>
|
||||
<TextField
|
||||
label="Email"
|
||||
value={localEmail}
|
||||
onChange={(e) => {
|
||||
setLocalEmail(e.target.value);
|
||||
setEmail(e.target.value);
|
||||
}}
|
||||
size="medium"
|
||||
className="text-field-email"
|
||||
style={{ width: "350px" }}
|
||||
/>
|
||||
<div className="fetch-button-container">
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
fetchData().catch((error: unknown) =>
|
||||
console.error("Fetch data error:", error),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<label>
|
||||
Email id:
|
||||
<input
|
||||
type="text"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="fetch-button"
|
||||
style={{
|
||||
padding: "10px",
|
||||
margin: "10px",
|
||||
width: "100%",
|
||||
padding: "0 16px",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
>
|
||||
FETCH
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="fetch-button">
|
||||
<button
|
||||
onClick={fetchData}
|
||||
style={{
|
||||
padding: "10px 20px",
|
||||
fontSize: "16px",
|
||||
cursor: "pointer",
|
||||
backgroundColor: "#009879",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "5px",
|
||||
}}
|
||||
>
|
||||
FETCH
|
||||
</button>
|
||||
<div className="content-container">
|
||||
{loading ? (
|
||||
<CircularProgress sx={{ color: "black" }} />
|
||||
) : error ? (
|
||||
<div className="error-message">{error}</div>
|
||||
) : fetchSuccess ? (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: "600px",
|
||||
bgcolor: "#FAFAFA",
|
||||
marginTop: "300px",
|
||||
borderRadius: "7px",
|
||||
position: "relative",
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
value={tabValue}
|
||||
onChange={handleTabChange}
|
||||
centered
|
||||
sx={{
|
||||
"& .MuiTabs-indicator": {
|
||||
backgroundColor: "#00B33C",
|
||||
height: "5px",
|
||||
borderRadius: "20px",
|
||||
},
|
||||
"& .MuiTab-root": {
|
||||
textTransform: "none",
|
||||
},
|
||||
"& .Mui-selected": {
|
||||
color: "black !important",
|
||||
},
|
||||
"& .MuiTab-root.Mui-selected": {
|
||||
color: "black !important",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab label="User" />
|
||||
<Tab label="Family" />
|
||||
<Tab label="Bonuses" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: "600px",
|
||||
mt: 4,
|
||||
minHeight: "400px",
|
||||
}}
|
||||
>
|
||||
{tabValue === 0 && (
|
||||
<UserComponent userData={userData} />
|
||||
)}
|
||||
{tabValue === 1 && <div>Family tab content</div>}
|
||||
{tabValue === 2 && <div>Bonuses tab content</div>}
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<div className="duckie-container">
|
||||
<img
|
||||
src={duckieimage}
|
||||
alt="Duckie"
|
||||
className="duckie-image"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<br />
|
||||
{error && <p style={{ color: "red" }}>{`Error: ${error}`}</p>}
|
||||
{userData && (
|
||||
<table
|
||||
style={{
|
||||
width: "100%",
|
||||
borderCollapse: "collapse",
|
||||
margin: "20px 0",
|
||||
fontSize: "1em",
|
||||
minWidth: "400px",
|
||||
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
|
||||
}}
|
||||
>
|
||||
<tbody>
|
||||
{Object.keys(userData).map((category) => (
|
||||
<React.Fragment key={category}>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={2}
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
backgroundColor: "#f1f1f1",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
{category.toUpperCase()}
|
||||
</td>
|
||||
</tr>
|
||||
{renderAttributes(userData[category])}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<footer className="footer">
|
||||
<p>
|
||||
<a href="https://help.ente.io">help.ente.io</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
192
infra/staff/src/components/ChangeEmail.tsx
Normal file
192
infra/staff/src/components/ChangeEmail.tsx
Normal file
@@ -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<ChangeEmailProps> = ({ open, onClose }) => {
|
||||
const [newEmail, setNewEmail] = useState<string>("");
|
||||
const [userID, setUserID] = useState<string>("");
|
||||
|
||||
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<HTMLInputElement>) => {
|
||||
setNewEmail(event.target.value); // Update newEmail state on input change
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
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<HTMLFormElement> = (
|
||||
event,
|
||||
) => {
|
||||
handleSubmit(event).catch((error: unknown) => {
|
||||
console.error("Error in handleSubmit:", error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
BackdropProps={{
|
||||
style: {
|
||||
backdropFilter: "blur(5px)",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.8)",
|
||||
},
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
width: "444px",
|
||||
height: "300px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle style={{ marginBottom: "20px", marginTop: "20px" }}>
|
||||
Change Email
|
||||
<Button
|
||||
onClick={onClose}
|
||||
style={{ position: "absolute", right: 10, top: 10 }}
|
||||
>
|
||||
<CloseIcon style={{ color: "black" }} />
|
||||
</Button>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<form onSubmit={handleSubmitSync}>
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<label
|
||||
htmlFor="newEmail"
|
||||
style={{
|
||||
textAlign: "left",
|
||||
display: "block",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<TextField
|
||||
id="newEmail"
|
||||
name="newEmail"
|
||||
value={newEmail}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogActions
|
||||
style={{ justifyContent: "center", marginTop: "40px" }}
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
style={{
|
||||
backgroundColor: "#00B33C",
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
Change Email
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangeEmail;
|
||||
100
infra/staff/src/components/DeleteAccont.tsx
Normal file
100
infra/staff/src/components/DeleteAccont.tsx
Normal file
@@ -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<DeleteAccountProps> = ({ 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 (
|
||||
<div>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
PaperComponent={Paper}
|
||||
sx={{
|
||||
width: "499px",
|
||||
height: "286px",
|
||||
margin: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
BackdropProps={{
|
||||
style: {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.9)", // Semi-transparent backdrop
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{"Delete Account?"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Are you sure you want to delete the account?
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ justifyContent: "center" }}>
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
sx={{
|
||||
bgcolor: "white",
|
||||
color: "black",
|
||||
"&:hover": { bgcolor: "#FAFAFA" },
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleDelete().catch((error: unknown) =>
|
||||
console.error("Fetch data error:", error),
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
bgcolor: "#F4473D",
|
||||
color: "white",
|
||||
"&:hover": { bgcolor: "#E53935" },
|
||||
}}
|
||||
>
|
||||
Delete{" "}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteAccount;
|
||||
156
infra/staff/src/components/Disable2FA.tsx
Normal file
156
infra/staff/src/components/Disable2FA.tsx
Normal file
@@ -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<Disable2FAProps> = ({
|
||||
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 (
|
||||
<div>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
PaperComponent={Paper}
|
||||
sx={{
|
||||
width: "499px",
|
||||
height: "286px",
|
||||
margin: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
BackdropProps={{
|
||||
style: {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.9)", // Semi-transparent backdrop
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{"Disable 2FA?"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Are you sure you want to disable 2FA for this account?
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ justifyContent: "center" }}>
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
sx={{
|
||||
bgcolor: "white",
|
||||
color: "black",
|
||||
"&:hover": { bgcolor: "#FAFAFA" },
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleDisable().catch((error: unknown) =>
|
||||
console.error(error),
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
bgcolor: "#F4473D",
|
||||
color: "white",
|
||||
"&:hover": { bgcolor: "#E53935" },
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Disabling..." : "Disable"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Disable2FA;
|
||||
367
infra/staff/src/components/UpdateSubscription.tsx
Normal file
367
infra/staff/src/components/UpdateSubscription.tsx
Normal file
@@ -0,0 +1,367 @@
|
||||
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 { 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 {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface FormValues {
|
||||
productId: string;
|
||||
provider: string;
|
||||
storage: number;
|
||||
transactionId: string;
|
||||
expiryTime: string | Date | null;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
const UpdateSubscription: React.FC<UpdateSubscriptionProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
}) => {
|
||||
const [values, setValues] = useState<FormValues>({
|
||||
productId: "",
|
||||
provider: "",
|
||||
storage: 0,
|
||||
transactionId: "",
|
||||
expiryTime: "",
|
||||
userId: "",
|
||||
});
|
||||
|
||||
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
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 response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
const userDataResponse =
|
||||
(await response.json()) as UserDataResponse;
|
||||
|
||||
if (!userDataResponse.subscription) {
|
||||
throw new Error("Subscription data not found");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData().catch((error: unknown) => {
|
||||
console.error("Unhandled promise rejection:", error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleCalendarClick = () => {
|
||||
setIsDatePickerOpen(true);
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDatePickerChange = (date: Date | null) => {
|
||||
setValues({
|
||||
...values,
|
||||
expiryTime: date,
|
||||
});
|
||||
setIsDatePickerOpen(false);
|
||||
};
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
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 (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
BackdropProps={{
|
||||
style: {
|
||||
backdropFilter: "blur(5px)",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.8)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle style={{ marginBottom: "20px", marginTop: "20px" }}>
|
||||
Update Subscription
|
||||
<Button
|
||||
onClick={onClose}
|
||||
style={{ position: "absolute", right: 10, top: 10 }}
|
||||
>
|
||||
<CloseIcon style={{ color: "black" }} />
|
||||
</Button>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Grid container spacing={4}>
|
||||
<Grid item xs={6}>
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<label
|
||||
htmlFor="productId"
|
||||
style={{
|
||||
textAlign: "left",
|
||||
display: "block",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
Product ID
|
||||
</label>
|
||||
<TextField
|
||||
id="productId"
|
||||
name="productId"
|
||||
value={values.productId}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<label
|
||||
htmlFor="provider"
|
||||
style={{
|
||||
textAlign: "left",
|
||||
display: "block",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
Provider
|
||||
</label>
|
||||
<Select
|
||||
id="provider"
|
||||
name="provider"
|
||||
value={values.provider}
|
||||
onChange={handleChangeProvider}
|
||||
fullWidth
|
||||
style={{ textAlign: "left" }}
|
||||
>
|
||||
<MenuItem value="stripe">Stripe</MenuItem>
|
||||
<MenuItem value="paypal">PayPal</MenuItem>
|
||||
<MenuItem value="bitpay">BitPay</MenuItem>
|
||||
</Select>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<label
|
||||
htmlFor="storage"
|
||||
style={{
|
||||
textAlign: "left",
|
||||
display: "block",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
Storage (GB)
|
||||
</label>
|
||||
<TextField
|
||||
id="storage"
|
||||
name="storage"
|
||||
type="number"
|
||||
value={values.storage}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<label
|
||||
htmlFor="transactionId"
|
||||
style={{
|
||||
textAlign: "left",
|
||||
display: "block",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
Transaction ID
|
||||
</label>
|
||||
<TextField
|
||||
id="transactionId"
|
||||
name="transactionId"
|
||||
value={values.transactionId}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<label
|
||||
htmlFor="expiryTime"
|
||||
style={{
|
||||
textAlign: "left",
|
||||
display: "block",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
Expiry Time
|
||||
</label>
|
||||
<TextField
|
||||
id="expiryTime"
|
||||
name="expiryTime"
|
||||
value={
|
||||
values.expiryTime instanceof Date
|
||||
? values.expiryTime.toLocaleDateString(
|
||||
"en-GB",
|
||||
)
|
||||
: ""
|
||||
}
|
||||
onClick={handleCalendarClick}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<CalendarTodayIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
readOnly: true,
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
{isDatePickerOpen && (
|
||||
<DatePicker
|
||||
showYearDropdown
|
||||
scrollableYearDropdown
|
||||
yearDropdownItemNumber={15}
|
||||
selected={
|
||||
values.expiryTime instanceof Date
|
||||
? values.expiryTime
|
||||
: null
|
||||
}
|
||||
onChange={handleDatePickerChange}
|
||||
onClickOutside={() =>
|
||||
setIsDatePickerOpen(false)
|
||||
}
|
||||
withPortal
|
||||
inline
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<DialogActions style={{ justifyContent: "center" }}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
style={{
|
||||
backgroundColor: "#00B33C",
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateSubscription;
|
||||
335
infra/staff/src/components/UserComponent.tsx
Normal file
335
infra/staff/src/components/UserComponent.tsx
Normal file
@@ -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<string, string>;
|
||||
Storage: Record<string, string>;
|
||||
Subscription: Record<string, string>;
|
||||
Security: Record<string, string>;
|
||||
}
|
||||
|
||||
interface UserComponentProps {
|
||||
userData: UserData | null;
|
||||
}
|
||||
|
||||
const UserComponent: React.FC<UserComponentProps> = ({ 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 (
|
||||
<Grid container spacing={6} justifyContent="center">
|
||||
{Object.entries(userData).map(([title, data]) => (
|
||||
<Grid item xs={12} sm={10} md={6} key={title}>
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
minHeight: 300,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
marginBottom: "20px",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
padding: "13px",
|
||||
"&:not(:last-child)": {
|
||||
marginBottom: "40px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "16px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="div"
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{title === "User" && (
|
||||
<IconButton
|
||||
edge="start"
|
||||
aria-label="delete"
|
||||
onClick={handleDeleteAccountClick}
|
||||
>
|
||||
<DeleteIcon style={{ color: "" }} />
|
||||
</IconButton>
|
||||
)}
|
||||
{title === "Subscription" && (
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="edit"
|
||||
onClick={handleEditSubscription}
|
||||
>
|
||||
<EditIcon style={{ color: "black" }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Table
|
||||
sx={{
|
||||
width: "100%",
|
||||
tableLayout: "fixed",
|
||||
height: "100%",
|
||||
borderBottom: "none",
|
||||
}}
|
||||
aria-label={title}
|
||||
>
|
||||
<TableBody>
|
||||
{Object.entries(
|
||||
data as Record<string, string>,
|
||||
).map(([label, value], index) => (
|
||||
<TableRow key={label}>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
style={{
|
||||
padding: "16px",
|
||||
borderBottom:
|
||||
index === 1 || index === 0
|
||||
? "1px solid rgba(224, 224, 224, 1)"
|
||||
: "none",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
style={{
|
||||
padding: "10px",
|
||||
borderBottom:
|
||||
index === 1 || index === 0
|
||||
? "1px solid rgba(224, 224, 224, 1)"
|
||||
: "none",
|
||||
}}
|
||||
>
|
||||
{label === "Email" ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent:
|
||||
"flex-end",
|
||||
}}
|
||||
>
|
||||
<Typography>
|
||||
{value}
|
||||
</Typography>
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="edit-email"
|
||||
onClick={
|
||||
handleEditEmail
|
||||
}
|
||||
>
|
||||
<EditIcon
|
||||
style={{
|
||||
color: "black",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
) : typeof value === "string" ? (
|
||||
label === "Two factor 2FA" ? (
|
||||
is2FADisabled ||
|
||||
value === "Disabled" ? (
|
||||
<Typography
|
||||
sx={{
|
||||
textAlign:
|
||||
"center",
|
||||
width: "100%",
|
||||
paddingLeft:
|
||||
"30px",
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems:
|
||||
"center",
|
||||
justifyContent:
|
||||
"center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography>
|
||||
{value}
|
||||
</Typography>
|
||||
{value ===
|
||||
"Enabled" && (
|
||||
<Switch
|
||||
checked={
|
||||
twoFactorEnabled
|
||||
}
|
||||
onChange={(
|
||||
e,
|
||||
) => {
|
||||
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",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
) : (
|
||||
<Typography>
|
||||
{value}
|
||||
</Typography>
|
||||
)
|
||||
) : (
|
||||
<Typography>
|
||||
{String(value)}
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Grid>
|
||||
))}
|
||||
|
||||
{/* Render DeleteAccount dialog */}
|
||||
<DeleteAccount
|
||||
open={deleteAccountOpen}
|
||||
handleClose={handleCloseDeleteAccount}
|
||||
/>
|
||||
|
||||
{/* Render Disable2FA dialog */}
|
||||
<Disable2FA
|
||||
open={disable2FAOpen}
|
||||
handleClose={handleCancelDisable2FA}
|
||||
handleDisable2FA={handleDisable2FA}
|
||||
/>
|
||||
|
||||
{/* Render UpdateSubscription dialog */}
|
||||
<UpdateSubscription
|
||||
open={updateSubscriptionOpen}
|
||||
onClose={handleCloseUpdateSubscription}
|
||||
/>
|
||||
|
||||
{/* Render ChangeEmail dialog */}
|
||||
<ChangeEmail
|
||||
open={changeEmailOpen}
|
||||
onClose={handleCloseChangeEmail}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserComponent;
|
||||
BIN
infra/staff/src/components/duckie.png
Normal file
BIN
infra/staff/src/components/duckie.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
@@ -1,10 +1,10 @@
|
||||
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");
|
||||
if (!root) throw new Error("Could not load root element to render onto");
|
||||
if (!root) throw new Error("Could not load root element to qrender onto");
|
||||
|
||||
ReactDOM.createRoot(root).render(
|
||||
<React.StrictMode>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const apiOrigin = import.meta.env.VITE_ENTE_API_ORIGIN ?? "https://api.ente.io";
|
||||
export const apiOrigin =
|
||||
import.meta.env.VITE_ENTE_API_ORIGIN ?? "https://api.ente.io";
|
||||
|
||||
const UserDetails = z.object({}).passthrough();
|
||||
|
||||
|
||||
4
infra/staff/src/vite-env.d.ts
vendored
4
infra/staff/src/vite-env.d.ts
vendored
@@ -9,7 +9,9 @@ interface ImportMetaEnv {
|
||||
* Override the origin (scheme://host:port) of Ente's API to connect to.
|
||||
*
|
||||
* Default is "https://api.ente.io".
|
||||
*/
|
||||
|
||||
*/
|
||||
|
||||
readonly VITE_ENTE_API_ORIGIN: string | undefined;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user