Compare commits

..

19 Commits

Author SHA1 Message Date
atyabbin
da0832fc7d Deleted fileee from <branch-name> 2024-07-16 17:15:58 +05:30
atyabbin
827ac9ddf7 Dashboard with imporved UI 2024-07-16 16:21:59 +05:30
atyabbin
2e35b1eeb4 First view of the dashboard with new UI 2024-07-06 16:33:26 +05:30
atyabbin
65c72f6cf5 Showing usage data in GB in the fetch table 2024-06-27 15:59:39 +05:30
atyabbin
f11cc82e44 modification in URL for update subscription 2024-06-27 14:16:29 +05:30
atyabbin
206e387834 Modified calender for date picking 2024-06-23 11:37:20 +05:30
atyabbin
567dfb7e6b Added options for delete account and update subscription 2024-06-23 11:13:59 +05:30
atyabbin
b7d3e5439a Pressing enter key will call fetchdata 2024-06-19 12:09:55 +05:30
atyabbin
ef37a4cad8 Reverting the changes in local.yaml 2024-06-19 09:56:59 +05:30
atyabbin
480a86af0a Changed the label of the new Button from 'More' to 'MORE' 2024-06-18 19:00:32 +05:30
atyabbin
a5b0bc259d Added a dropdown button to select different actions to be performed 2024-06-18 18:40:40 +05:30
atyabbin
326e673d12 Added display message after button clicks 2024-06-18 14:25:37 +05:30
atyabbin
00b68131a8 Solved the lint issues 2024-06-18 13:21:37 +05:30
atyabbin
de8d81300f Modified the Sidebar Component 2024-06-18 13:10:19 +05:30
atyabbin
e287e80257 Added new buttons in the dashboard 2024-06-18 09:56:00 +05:30
atyabbin
d716f18c2e Added buttons for disable2fa, close family and disable passkeys 2024-06-18 09:38:36 +05:30
atyabbin
93bddbe6f1 Merge branch 'main' into dashboard-improvement 2024-06-10 19:41:29 +05:30
atyabbin
17e48ed83f Added new components 2024-06-10 19:29:28 +05:30
atyabbin
4c7583240f Sidebar added 2024-06-10 19:24:32 +05:30
41 changed files with 7959 additions and 655 deletions

View File

@@ -4,7 +4,7 @@ on:
workflow_dispatch: # Allow manually running the action
env:
FLUTTER_VERSION: "3.22.2"
FLUTTER_VERSION: "3.22.1"
jobs:
build:

View File

@@ -10,7 +10,7 @@ on:
env:
FLUTTER_VERSION: "3.22.2"
FLUTTER_VERSION: "3.22.1"
jobs:
lint:

View File

@@ -9,7 +9,7 @@ on:
- "photos-v*"
env:
FLUTTER_VERSION: "3.22.2"
FLUTTER_VERSION: "3.22.1"
jobs:
build:

View File

@@ -1,54 +1,51 @@
# Security Policy
Ente believes that working with security researchers across the globe is crucial
to keeping our users safe. If you believe you've found a security issue in our
product or service, we encourage you to notify us by email at security@ente.io
or by
[filling out this form](https://github.com/ente-io/ente/security/advisories/new).
We welcome working with you to resolve the issue promptly. Thanks in advance!
product or service, we encourage you to notify us, by email (security@ente.io)
or by [filling this
form](https://github.com/ente-io/ente/security/advisories/new) We welcome
working with you to resolve the issue promptly. Thanks in advance!
## Disclosure Policy
- Let us know as soon as possible upon discovery of a potential security
issue, and we'll make every effort to quickly resolve the issue.
- Provide us with a reasonable amount of time to resolve the issue before any
disclosure to the public or a third party. We may publicly disclose the
issue before resolving it if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data,
and interruption or degradation of our service. Only interact with accounts
you own or with the explicit permission of the account holder.
- If you would like to encrypt your report, please use the PGP key with long
ID `E273695C0403F34F74171932DF6DDDE98EBD2394` (available in the public
keyserver pool).
- Let us know as soon as possible upon discovery of a potential security issue,
and we'll make every effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any
disclosure to the public or a third-party. We may publicly disclose the issue
before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and
interruption or degradation of our service. Only interact with accounts you
own or with explicit permission of the account holder.
- If you would like to encrypt your report, please use the PGP key with long ID
`E273695C0403F34F74171932DF6DDDE98EBD2394` (available in the public keyserver
pool).
## In-scope
- Security issues in any current release of Ente's services. Product downloads
are available at [https://ente.io](https://ente.io). Source code is
available at [https://github.com/ente-io](https://github.com/ente-io).
- Security issues in any current release of Ente's services. Product downloads
are available at https://ente.io. Source code is available at
https://github.com/ente-io.
## Exclusions
The following bug classes are out of scope:
The following bug classes are out-of scope:
- Bugs that are already reported on any of
[Ente's issue trackers](https://github.com/ente-io) or that we already know
of (note that some of our issue tracking is private).
- Issues in an upstream software dependency (e.g., Flutter, Next.js, etc.)
that are already reported to the upstream maintainer.
- Attacks requiring physical access to a user's device.
- Self-XSS.
- Issues related to software or protocols not under Ente's control.
- Vulnerabilities in outdated versions of Ente.
- Missing security best practices that do not directly lead to a
vulnerability.
- Issues that do not have any impact on the general public.
- Bugs that are already reported on any of [Ente's issue
trackers](https://github.com/ente-io), or that we already know of (Note that
some of our issue tracking is private)
- Issues in an upstream software dependency (ex: Flutter, Next.js etc) which are
already reported to the upstream maintainer
- Attacks requiring physical access to a user's device
- Self-XSS
- Issues related to software or protocols not under ente's control
- Vulnerabilities in outdated versions of ente
- Missing security best practices that do not directly lead to a vulnerability
- Issues that do not have any impact on the general public
While researching, we'd like to ask you to refrain from:
- Denial of service
- Spamming
- Social engineering (including phishing) of Ente staff or contractors
- Any physical attempts against Ente property or data centers
- Denial of service
- Spamming
- Social engineering (including phishing) of Ente staff or contractors
- Any physical attempts against Ente property or data centers
Thank you for helping keep Ente and our users safe!

View File

@@ -42,7 +42,7 @@ class PasskeyService {
Future<void> openPasskeyPage(BuildContext context) async {
try {
final jwtToken = await getJwtToken();
final url = "https://accounts.ente.io/passkeys/handoff?token=$jwtToken";
final url = "https://accounts.ente.io/account-handoff?token=$jwtToken";
await launchUrlString(
url,
mode: LaunchMode.externalApplication,

View File

@@ -41,10 +41,9 @@ class _PasskeyPageState extends State<PasskeyPage> {
Future<void> launchPasskey() async {
await launchUrlString(
"https://accounts.ente.io/passkeys/verify?"
"https://accounts.ente.io/passkeys/flow?"
"passkeySessionID=${widget.sessionID}"
"&redirect=enteauth://passkey"
"&clientPackage=io.ente.auth",
"&redirect=enteauth://passkey",
mode: LaunchMode.externalApplication,
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -71,15 +71,12 @@ func NewClient(p Params) *Client {
restClient: enteAPI,
downloadClient: resty.New().
SetRetryCount(3).
SetRetryWaitTime(10 * time.Second).
SetRetryMaxWaitTime(20 * time.Second).
SetRetryWaitTime(5 * time.Second).
SetRetryMaxWaitTime(10 * time.Second).
AddRetryCondition(func(r *resty.Response, err error) bool {
shouldRetry := r.StatusCode() == 429 || r.StatusCode() >= 500
shouldRetry := r.StatusCode() == 429 || r.StatusCode() > 500
if shouldRetry {
amxRequestID := r.Header().Get("X-Amz-Request-Id")
cfRayID := r.Header().Get("CF-Ray")
wasabiRefID := r.Header().Get("X-Wasabi-Cm-Reference-Id")
log.Printf("Retry scheduled. error statusCode: %d, X-Amz-Request-Id: %s, CF-Ray: %s, X-Wasabi-Cm-Reference-Id: %s", r.StatusCode(), amxRequestID, cfRayID, wasabiRefID)
log.Printf("retrying download due to %d code", r.StatusCode())
}
return shouldRetry
}),

View File

@@ -15,7 +15,7 @@ import (
"strings"
)
var AppVersion = "0.1.15"
var AppVersion = "0.1.14"
func main() {
cliDBPath, err := GetCLIConfigPath()

View File

@@ -1,6 +1,6 @@
name: "Release"
# Build the desktop app with code from ente-io/ente and create/update a release.
# Build the ente-io/ente's desktop/rc branch and create/update a draft release.
#
# For more details, see `docs/release.md` in ente-io/ente.

View File

@@ -4,8 +4,6 @@
- Remember the window size across app restarts.
- Revert changes to the Linux icon.
- Fix an issue where deleted items in watched folders would not move to
uncategorized.
## v1.7.0

5623
infra/staff/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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;
}

View File

@@ -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;

View 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;

View 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;

View 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;

View 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;

View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -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>

View File

@@ -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();

View File

@@ -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

View File

@@ -46,7 +46,7 @@ You can alternatively install the build from PlayStore or F-Droid.
## 🧑‍💻 Building from source
1. [Install Flutter v3.22.2](https://flutter.dev/docs/get-started/install).
1. [Install Flutter v3.22.0](https://flutter.dev/docs/get-started/install).
2. Pull in all submodules with `git submodule update --init --recursive`

View File

@@ -42,7 +42,7 @@ class PasskeyService {
Future<void> openPasskeyPage(BuildContext context) async {
try {
final jwtToken = await getJwtToken();
final url = "https://accounts.ente.io/passkeys/handoff?token=$jwtToken";
final url = "https://accounts.ente.io/account-handoff?token=$jwtToken";
await launchUrlString(
url,
mode: LaunchMode.externalApplication,

View File

@@ -41,10 +41,9 @@ class _PasskeyPageState extends State<PasskeyPage> {
Future<void> launchPasskey() async {
await launchUrlString(
"https://accounts.ente.io/passkeys/verify?"
"https://accounts.ente.io/passkeys/flow?"
"passkeySessionID=${widget.sessionID}"
"&redirect=ente://passkey"
"&clientPackage=io.ente.photos",
"&redirect=ente://passkey",
mode: LaunchMode.externalApplication,
);
}

View File

@@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.8.135+655
version: 0.8.134+654
publish_to: none
environment:

View File

@@ -1,4 +1,4 @@
FROM golang:1.21-alpine3.17 as builder
FROM golang:1.20-alpine3.17 as builder
RUN apk add --no-cache gcc musl-dev git build-base pkgconfig libsodium-dev
ENV GOOS=linux

View File

@@ -487,7 +487,7 @@ func main() {
accountsJwtAuthAPI.GET("/passkeys", passkeysHandler.GetPasskeys)
accountsJwtAuthAPI.PATCH("/passkeys/:passkeyID", passkeysHandler.RenamePasskey)
accountsJwtAuthAPI.DELETE("/passkeys/:passkeyID", passkeysHandler.DeletePasskey)
accountsJwtAuthAPI.POST("/passkeys/registration/begin", passkeysHandler.BeginRegistration)
accountsJwtAuthAPI.GET("/passkeys/registration/begin", passkeysHandler.BeginRegistration)
accountsJwtAuthAPI.POST("/passkeys/registration/finish", passkeysHandler.FinishRegistration)
collectionHandler := &api.CollectionHandler{

View File

@@ -233,16 +233,11 @@ stripe:
success: ?status=success&session_id={CHECKOUT_SESSION_ID}
cancel: ?status=fail&reason=canceled
# Passkey support (optional)
# Use case: MFA
# Passkey support (WIP)
webauthn:
# Our "Relying Party" ID. This scopes the generated credentials.
# See: https://www.w3.org/TR/webauthn-3/#rp-id
rpid: localhost
# Whitelist of origins from where we will accept WebAuthn requests.
# See: https://github.com/go-webauthn/webauthn
rpid: "example.com"
rporigins:
- "http://localhost:3001"
- "https://example.com:3005"
# Roadmap SSO (optional)
#

View File

@@ -1,7 +1,6 @@
module github.com/ente-io/museum
go 1.21
go 1.20
require (
firebase.google.com/go v3.13.0+incompatible
@@ -20,7 +19,7 @@ require (
github.com/golang-jwt/jwt v3.2.1+incompatible
github.com/golang-migrate/migrate/v4 v4.12.2
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.6.0
github.com/google/uuid v1.4.0
github.com/kong/go-srp v0.0.0-20191210190804-cde1efa3c083
github.com/lib/pq v1.8.0
github.com/lithammer/shortuuid/v3 v3.0.4
@@ -31,12 +30,12 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.6.0
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.8.4
github.com/stripe/stripe-go/v72 v72.37.0
github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f
github.com/ulule/limiter/v3 v3.8.0
github.com/zsais/go-gin-prometheus v0.1.0
golang.org/x/crypto v0.21.0
golang.org/x/crypto v0.17.0
golang.org/x/sync v0.1.0
golang.org/x/text v0.14.0
google.golang.org/api v0.114.0
@@ -48,11 +47,11 @@ require (
cloud.google.com/go/longrunning v0.4.1 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-webauthn/x v0.1.9 // indirect
github.com/go-webauthn/x v0.1.5 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
github.com/google/go-tpm v0.9.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
@@ -79,7 +78,7 @@ require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-webauthn/webauthn v0.10.2
github.com/go-webauthn/webauthn v0.9.4
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/googleapis/gax-go/v2 v2.7.1 // indirect
@@ -109,9 +108,9 @@ require (
github.com/subosito/gotenv v1.2.0 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect

View File

@@ -164,8 +164,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw=
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -193,7 +193,6 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
@@ -208,10 +207,10 @@ github.com/go-redis/redis/v8 v8.4.2/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hb
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-webauthn/webauthn v0.10.2 h1:OG7B+DyuTytrEPFmTX503K77fqs3HDK/0Iv+z8UYbq4=
github.com/go-webauthn/webauthn v0.10.2/go.mod h1:Gd1IDsGAybuvK1NkwUTLbGmeksxuRJjVN2PE/xsPxHs=
github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE=
github.com/go-webauthn/x v0.1.9/go.mod h1:pJNMlIMP1SU7cN8HNlKJpLEnFHCygLCvaLZ8a1xeoQA=
github.com/go-webauthn/webauthn v0.9.4 h1:YxvHSqgUyc5AK2pZbqkWWR55qKeDPhP8zLDr6lpIc2g=
github.com/go-webauthn/webauthn v0.9.4/go.mod h1:LqupCtzSef38FcxzaklmOn7AykGKhAhr9xlRbdbgnTw=
github.com/go-webauthn/x v0.1.5 h1:V2TCzDU2TGLd0kSZOXdrqDVV5JB9ILnKxA9S53CSBw0=
github.com/go-webauthn/x v0.1.5/go.mod h1:qbzWwcFcv4rTwtCLOZd+icnr6B7oSsAGZJqlt8cukqY=
github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
@@ -224,8 +223,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-migrate/migrate/v4 v4.12.2 h1:QI43Tlouiwpp2dK5Y767OouX0snJNRP/NubsVaArzDU=
github.com/golang-migrate/migrate/v4 v4.12.2/go.mod h1:HQ1DaC8uLHkg4afY8ZQ8D/P5SG+YW9X5INZBVvm+d2k=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@@ -293,7 +292,6 @@ github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIG
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -311,8 +309,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -579,8 +577,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stripe/stripe-go/v72 v72.37.0 h1:y/PW0SeIk17S1uq6tQ0RdyeizG1anZlvowMZ4AQ17YY=
github.com/stripe/stripe-go/v72 v72.37.0/go.mod h1:QwqJQtduHubZht9mek5sds9CtQcKFdsykV9ZepRWwo0=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
@@ -651,8 +649,8 @@ golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -734,8 +732,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -830,8 +828,8 @@ golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -60,6 +60,9 @@ func NewRepository(
db *sql.DB,
) (repo *Repository, err error) {
rpId := viper.GetString("webauthn.rpid")
if rpId == "" {
rpId = "accounts.ente.io"
}
rpOrigins := viper.GetStringSlice("webauthn.rporigins")
wconfig := &webauthn.Config{
@@ -69,7 +72,7 @@ func NewRepository(
Timeouts: webauthn.TimeoutsConfig{
Login: webauthn.TimeoutConfig{
Enforce: true,
Timeout: time.Duration(2) * time.Minute,
Timeout: time.Duration(5) * time.Minute,
},
Registration: webauthn.TimeoutConfig{
Enforce: true,

View File

@@ -1,5 +1,6 @@
import { setClientPackageForAuthenticatedRequests } from "@/next/http";
import log from "@/next/log";
import { clientPackageName } from "@/next/types/app";
import type { TwoFactorAuthorizationResponse } from "@/next/types/credentials";
import { ensure } from "@/utils/ensure";
import { nullToUndefined } from "@/utils/transform";
@@ -60,12 +61,15 @@ const Page = () => {
// The server needs to know the app on whose behalf we're trying to
// authenticate.
const clientPackage = nullToUndefined(
searchParams.get("clientPackage"),
);
let clientPackage = nullToUndefined(searchParams.get("client"));
// Mobile apps don't pass the client header, deduce their client package
// name from the redirect URL that they provide.
if (!clientPackage) {
setStatus("unrecoverableFailure");
return;
// TODO-PK: Pass from mobile app too?
clientPackage =
clientPackageName[
redirectURL.protocol == "enteauth:" ? "auth" : "photos"
];
}
localStorage.setItem("clientPackage", clientPackage);
@@ -103,7 +107,6 @@ const Page = () => {
authorizationResponse = await finishPasskeyAuthentication({
passkeySessionID,
ceremonySessionID,
clientPackage,
credential,
});
} catch (e) {

View File

@@ -143,7 +143,6 @@ interface BeginPasskeyRegistrationResponse {
const beginPasskeyRegistration = async () => {
const url = `${apiOrigin()}/passkeys/registration/begin`;
const res = await fetch(url, {
method: "POST",
headers: accountsAuthenticatedRequestHeaders(),
});
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
@@ -413,22 +412,18 @@ export const beginPasskeyAuthentication = async (
*/
export const signChallenge = async (
publicKey: PublicKeyCredentialRequestOptions,
) => navigator.credentials.get({ publicKey });
) => {
// Allow up to 60 seconds to wait for the retrieval
publicKey.timeout = 60 * 1000;
return navigator.credentials.get({ publicKey });
};
interface FinishPasskeyAuthenticationOptions {
passkeySessionID: string;
ceremonySessionID: string;
/**
* The package name of the client on whose behalf we're authenticating with
* the user's passkey.
*
* This is used by the backend to generate an appropriately scoped auth
* token for used by (and only by) the authenticating app.
*/
clientPackage: string;
credential: Credential;
}
/**
* Finish the authentication by providing the signed assertion to the backend.
*
@@ -441,7 +436,6 @@ interface FinishPasskeyAuthenticationOptions {
export const finishPasskeyAuthentication = async ({
passkeySessionID,
ceremonySessionID,
clientPackage,
credential,
}: FinishPasskeyAuthenticationOptions) => {
const response = authenticatorAssertionResponse(credential);
@@ -458,14 +452,11 @@ export const finishPasskeyAuthentication = async ({
const params = new URLSearchParams({
sessionID: passkeySessionID,
ceremonySessionID,
clientPackage,
});
const url = `${apiOrigin()}/users/two-factor/passkeys/finish`;
const res = await fetch(`${url}?${params.toString()}`, {
method: "POST",
headers: {
"X-Client-Package": clientPackage,
},
headers: clientPackageHeaderIfPresent(),
body: JSON.stringify({
id: credential.id,
// This is meant to be the ArrayBuffer version of the (base64

View File

@@ -270,11 +270,10 @@ class FolderWatcher {
}
const [removed, rest] = watch.syncedFiles.reduce(
([removed, rest], syncedFile) => {
(event.filePaths.includes(syncedFile.path)
? removed
: rest
).push(syncedFile);
([removed, rest], { path }) => {
(event.filePaths.includes(path) ? rest : removed).push(
watch,
);
return [removed, rest];
},
[[], []],

View File

@@ -55,8 +55,9 @@ used.** This restriction is a byproduct of the enablement for automatic login.
### Automatically logging into Ente Accounts
Clients open a WebView with the URL
`https://accounts.ente.io/passkeys/handoff?token=<accountsToken>`. This page
will parse the token for usage in subsequent Accounts-related API calls.
`https://accounts.ente.io/passkeys/handoff?client=<clientPackageName>&token=<accountsToken>`.
This page will parse the token and client package name for usage in subsequent
Accounts-related API calls.
If the token is valid, the user will be automatically redirected to the passkeys
management page. Otherwise, they will be required to login with their Ente
@@ -107,7 +108,7 @@ func (u *PasskeyUser) WebAuthnCredentials() []webauthn.Credential {
}
```
#### POST /passkeys/registration/begin
#### GET /passkeys/registration/begin
##### Headers
@@ -341,14 +342,14 @@ is needed anyways to service credential authentication from mobile clients, so
we use the same flow for other (web, desktop) clients too.
```tsx
window.location.href = `${accountsAppURL()}/passkeys/verify?passkeySessionID=${passkeySessionID}&clientPackage=io.ente.photos.web&redirect=${
window.location.href = `${accountsAppURL()}/passkeys/verify?passkeySessionID=${passkeySessionID}&redirect=${
window.location.origin
}/passkeys/finish`;
```
### Requesting publicKey options (begin)
#### POST /users/two-factor/passkeys/begin
#### GET /users/two-factor/passkeys/begin
##### Query parameters

View File

@@ -28,13 +28,9 @@ export const redirectUserToPasskeyVerificationFlow = (
appName: AppName,
passkeySessionID: string,
) => {
const clientPackage = clientPackageName[appName];
const client = clientPackageName[appName];
const redirect = `${window.location.origin}/passkeys/finish`;
const params = new URLSearchParams({
clientPackage,
passkeySessionID,
redirect,
});
const params = new URLSearchParams({ client, passkeySessionID, redirect });
window.location.href = `${accountsAppURL()}/passkeys/verify?${params.toString()}`;
};

View File

@@ -610,8 +610,8 @@
"APPLY_CROP": "应用裁剪",
"PHOTO_EDIT_REQUIRED_TO_SAVE": "保存之前必须至少执行一项转换或颜色调整。",
"passkeys": "通行密钥",
"passkey_fetch_failed": "无法获取您的通行密钥。",
"manage_passkey": "管理通行密钥",
"passkey_fetch_failed": "",
"manage_passkey": "",
"delete_passkey": "删除通行密钥",
"delete_passkey_confirmation": "您确定要删除此通行密钥吗?此操作是不可逆的。",
"rename_passkey": "重命名通行密钥",
@@ -619,11 +619,11 @@
"enter_passkey_name": "输入该通行密钥的名称",
"passkeys_description": "通行密钥是您 Ente 账户的现代、安全的第二因素。通行密钥使用设备上的生物识别认证,这既方便又安全。",
"CREATED_AT": "创建于",
"passkey_add_failed": "无法添加通行密钥",
"passkey_add_failed": "",
"passkey_login_failed": "通行密钥登录失败",
"passkey_login_invalid_url": "该登录 URL 无效",
"passkey_login_generic_error": "使用通行密钥登录时出错。",
"passkeys_not_supported": "此浏览器不支持通行密钥",
"passkeys_not_supported": "",
"TRY_AGAIN": "重试",
"passkey_login_instructions": "按照浏览器中提示的步骤继续登录。",
"passkey_login": "使用通行密钥来登录",