1
0
Fork 0
mirror of https://github.com/binwiederhier/ntfy.git synced 2024-11-10 13:54:16 +01:00
ntfy/web/src/components/Account.js

799 lines
33 KiB
JavaScript
Raw Normal View History

2022-12-16 04:07:04 +01:00
import * as React from 'react';
2023-02-02 21:19:37 +01:00
import {useContext, useState} from 'react';
2023-01-28 05:10:59 +01:00
import {
Alert,
CardActions,
2023-02-02 21:19:37 +01:00
CardContent,
FormControl,
LinearProgress,
Link,
Portal,
Select,
Snackbar,
2023-01-28 05:10:59 +01:00
Stack,
2023-02-02 21:19:37 +01:00
Table,
TableBody,
TableCell,
2023-01-28 05:10:59 +01:00
TableHead,
TableRow,
useMediaQuery
} from "@mui/material";
2022-12-17 19:49:32 +01:00
import Tooltip from '@mui/material/Tooltip';
2022-12-16 04:07:04 +01:00
import Typography from "@mui/material/Typography";
import EditIcon from '@mui/icons-material/Edit';
import Container from "@mui/material/Container";
import Card from "@mui/material/Card";
import Button from "@mui/material/Button";
2023-01-28 05:10:59 +01:00
import {Trans, useTranslation} from "react-i18next";
2022-12-16 04:07:04 +01:00
import session from "../app/Session";
2022-12-17 19:49:32 +01:00
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
2022-12-16 04:07:04 +01:00
import theme from "./theme";
import Dialog from "@mui/material/Dialog";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import TextField from "@mui/material/TextField";
import routes from "./routes";
2022-12-17 19:49:32 +01:00
import IconButton from "@mui/material/IconButton";
2023-02-02 21:19:37 +01:00
import {formatBytes, formatShortDate, formatShortDateTime, openUrl} from "../app/utils";
import accountApi, {LimitBasis, Role, SubscriptionStatus} from "../app/AccountApi";
2023-01-05 21:20:44 +01:00
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
2022-12-29 14:20:53 +01:00
import {Pref, PrefGroup} from "./Pref";
2023-01-05 21:20:44 +01:00
import db from "../app/db";
2023-01-09 21:40:46 +01:00
import i18n from "i18next";
import humanizeDuration from "humanize-duration";
import UpgradeDialog from "./UpgradeDialog";
import CelebrationIcon from "@mui/icons-material/Celebration";
2023-01-10 02:37:13 +01:00
import {AccountContext} from "./App";
import DialogFooter from "./DialogFooter";
2023-01-28 05:10:59 +01:00
import {Paragraph} from "./styles";
import CloseIcon from "@mui/icons-material/Close";
2023-01-29 02:29:06 +01:00
import {ContentCopy, Public} from "@mui/icons-material";
2023-01-28 05:10:59 +01:00
import MenuItem from "@mui/material/MenuItem";
import DialogContentText from "@mui/material/DialogContentText";
2023-02-02 21:19:37 +01:00
import {IncorrectPasswordError, UnauthorizedError} from "../app/errors";
2022-12-16 04:07:04 +01:00
const Account = () => {
2022-12-19 15:59:32 +01:00
if (!session.exists()) {
window.location.href = routes.app;
return <></>;
}
2022-12-16 04:07:04 +01:00
return (
<Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}>
<Stack spacing={3}>
<Basics/>
2022-12-17 19:49:32 +01:00
<Stats/>
2023-01-28 05:10:59 +01:00
<Tokens/>
2022-12-17 19:49:32 +01:00
<Delete/>
2022-12-16 04:07:04 +01:00
</Stack>
</Container>
);
};
const Basics = () => {
const { t } = useTranslation();
return (
2022-12-29 08:32:05 +01:00
<Card sx={{p: 3}} aria-label={t("account_basics_title")}>
2022-12-16 04:07:04 +01:00
<Typography variant="h5" sx={{marginBottom: 2}}>
2022-12-29 08:32:05 +01:00
{t("account_basics_title")}
2022-12-16 04:07:04 +01:00
</Typography>
<PrefGroup>
2022-12-17 19:49:32 +01:00
<Username/>
2022-12-16 04:07:04 +01:00
<ChangePassword/>
<AccountType/>
2022-12-17 19:49:32 +01:00
</PrefGroup>
</Card>
);
};
const Username = () => {
const { t } = useTranslation();
2023-01-10 02:37:13 +01:00
const { account } = useContext(AccountContext);
2022-12-29 14:20:53 +01:00
const labelId = "prefUsername";
2022-12-17 19:49:32 +01:00
return (
2022-12-29 14:20:53 +01:00
<Pref labelId={labelId} title={t("account_basics_username_title")} description={t("account_basics_username_description")}>
<div aria-labelledby={labelId}>
2022-12-17 19:49:32 +01:00
{session.username()}
2023-01-30 19:10:45 +01:00
{account?.role === Role.ADMIN
2022-12-29 08:32:05 +01:00
? <>{" "}<Tooltip title={t("account_basics_username_admin_tooltip")}><span style={{cursor: "default"}}>👑</span></Tooltip></>
2022-12-17 19:49:32 +01:00
: ""}
</div>
</Pref>
)
};
2022-12-16 04:07:04 +01:00
const ChangePassword = () => {
const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
2022-12-29 14:20:53 +01:00
const labelId = "prefChangePassword";
2022-12-29 08:32:05 +01:00
2022-12-16 04:07:04 +01:00
const handleDialogOpen = () => {
setDialogKey(prev => prev+1);
setDialogOpen(true);
};
2022-12-29 08:32:05 +01:00
const handleDialogClose = () => {
2022-12-16 04:07:04 +01:00
setDialogOpen(false);
};
2022-12-29 08:32:05 +01:00
2022-12-16 04:07:04 +01:00
return (
2022-12-29 14:20:53 +01:00
<Pref labelId={labelId} title={t("account_basics_password_title")} description={t("account_basics_password_description")}>
<div aria-labelledby={labelId}>
2022-12-17 19:49:32 +01:00
<Typography color="gray" sx={{float: "left", fontSize: "0.7rem", lineHeight: "3.5"}}></Typography>
2022-12-29 08:32:05 +01:00
<IconButton onClick={handleDialogOpen} aria-label={t("account_basics_password_description")}>
2022-12-17 19:49:32 +01:00
<EditIcon/>
</IconButton>
</div>
2022-12-16 04:07:04 +01:00
<ChangePasswordDialog
key={`changePasswordDialog${dialogKey}`}
open={dialogOpen}
onClose={handleDialogClose}
2022-12-16 04:07:04 +01:00
/>
</Pref>
)
};
const ChangePasswordDialog = (props) => {
const { t } = useTranslation();
2023-02-02 21:19:37 +01:00
const [error, setError] = useState("");
const [currentPassword, setCurrentPassword] = useState("");
2022-12-16 04:07:04 +01:00
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const handleDialogSubmit = async () => {
try {
console.debug(`[Account] Changing password`);
await accountApi.changePassword(currentPassword, newPassword);
props.onClose();
} catch (e) {
console.log(`[Account] Error changing password`, e);
2023-02-02 21:19:37 +01:00
if (e instanceof IncorrectPasswordError) {
setError(t("account_basics_password_dialog_current_password_incorrect"));
} else if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
2023-02-02 21:19:37 +01:00
} else {
setError(e.message);
}
}
};
2022-12-16 04:07:04 +01:00
return (
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
2022-12-29 08:32:05 +01:00
<DialogTitle>{t("account_basics_password_dialog_title")}</DialogTitle>
2022-12-16 04:07:04 +01:00
<DialogContent>
<TextField
margin="dense"
id="current-password"
label={t("account_basics_password_dialog_current_password_label")}
aria-label={t("account_basics_password_dialog_current_password_label")}
type="password"
value={currentPassword}
onChange={ev => setCurrentPassword(ev.target.value)}
fullWidth
variant="standard"
/>
2022-12-16 04:07:04 +01:00
<TextField
margin="dense"
id="new-password"
2022-12-29 08:32:05 +01:00
label={t("account_basics_password_dialog_new_password_label")}
aria-label={t("account_basics_password_dialog_new_password_label")}
2022-12-16 04:07:04 +01:00
type="password"
value={newPassword}
onChange={ev => setNewPassword(ev.target.value)}
fullWidth
variant="standard"
/>
<TextField
margin="dense"
id="confirm"
2022-12-29 08:32:05 +01:00
label={t("account_basics_password_dialog_confirm_password_label")}
aria-label={t("account_basics_password_dialog_confirm_password_label")}
2022-12-16 04:07:04 +01:00
type="password"
value={confirmPassword}
onChange={ev => setConfirmPassword(ev.target.value)}
fullWidth
variant="standard"
/>
</DialogContent>
2023-02-02 21:19:37 +01:00
<DialogFooter status={error}>
2023-02-09 21:24:12 +01:00
<Button onClick={props.onClose}>{t("common_cancel")}</Button>
<Button
onClick={handleDialogSubmit}
disabled={newPassword.length === 0 || currentPassword.length === 0 || newPassword !== confirmPassword}
>
{t("account_basics_password_dialog_button_submit")}
</Button>
</DialogFooter>
2022-12-16 04:07:04 +01:00
</Dialog>
);
};
const AccountType = () => {
2022-12-29 08:32:05 +01:00
const { t } = useTranslation();
2023-01-10 02:37:13 +01:00
const { account } = useContext(AccountContext);
const [upgradeDialogKey, setUpgradeDialogKey] = useState(0);
2023-01-09 21:40:46 +01:00
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
2023-02-02 21:19:37 +01:00
const [showPortalError, setShowPortalError] = useState(false);
2023-01-14 12:43:44 +01:00
2022-12-29 08:32:05 +01:00
if (!account) {
return <></>;
}
2023-01-14 12:43:44 +01:00
const handleUpgradeClick = () => {
setUpgradeDialogKey(k => k + 1);
setUpgradeDialogOpen(true);
}
2023-01-14 12:43:44 +01:00
const handleManageBilling = async () => {
try {
const response = await accountApi.createBillingPortalSession();
2023-01-16 05:29:46 +01:00
window.open(response.redirect_url, "billing_portal");
2023-01-14 12:43:44 +01:00
} catch (e) {
2023-02-02 21:19:37 +01:00
console.log(`[Account] Error opening billing portal`, e);
if (e instanceof UnauthorizedError) {
2023-01-14 12:43:44 +01:00
session.resetAndRedirect(routes.login);
2023-02-02 21:19:37 +01:00
} else {
setShowPortalError(true);
2023-01-14 12:43:44 +01:00
}
}
};
let accountType;
2023-01-30 19:10:45 +01:00
if (account.role === Role.ADMIN) {
2023-02-09 21:24:12 +01:00
const tierSuffix = (account.tier) ? t("account_basics_tier_admin_suffix_with_tier", { tier: account.tier.name }) : t("account_basics_tier_admin_suffix_no_tier");
accountType = `${t("account_basics_tier_admin")} ${tierSuffix}`;
} else if (!account.tier) {
2023-02-09 21:24:12 +01:00
accountType = (config.enable_payments) ? t("account_basics_tier_free") : t("account_basics_tier_basic");
} else {
accountType = account.tier.name;
}
return (
<Pref
2023-01-30 19:10:45 +01:00
alignTop={account.billing?.status === SubscriptionStatus.PAST_DUE || account.billing?.cancel_at > 0}
2023-02-09 21:24:12 +01:00
title={t("account_basics_tier_title")}
description={t("account_basics_tier_description")}
>
<div>
{accountType}
{account.billing?.paid_until && !account.billing?.cancel_at &&
2023-02-09 21:24:12 +01:00
<Tooltip title={t("account_basics_tier_paid_until", { date: formatShortDate(account.billing?.paid_until) })}>
<span><InfoIcon/></span>
</Tooltip>
}
2023-01-30 19:10:45 +01:00
{config.enable_payments && account.role === Role.USER && !account.billing?.subscription &&
<Button
variant="outlined"
size="small"
startIcon={<CelebrationIcon sx={{ color: "#55b86e" }}/>}
onClick={handleUpgradeClick}
sx={{ml: 1}}
2023-02-09 21:24:12 +01:00
>{t("account_basics_tier_upgrade_button")}</Button>
}
2023-01-30 19:10:45 +01:00
{config.enable_payments && account.role === Role.USER && account.billing?.subscription &&
<Button
variant="outlined"
size="small"
onClick={handleUpgradeClick}
sx={{ml: 1}}
2023-02-09 21:24:12 +01:00
>{t("account_basics_tier_change_button")}</Button>
}
2023-01-30 19:10:45 +01:00
{config.enable_payments && account.role === Role.USER && account.billing?.customer &&
<Button
variant="outlined"
size="small"
onClick={handleManageBilling}
sx={{ml: 1}}
2023-02-09 21:24:12 +01:00
>{t("account_basics_tier_manage_billing_button")}</Button>
}
{config.enable_payments &&
<UpgradeDialog
key={`upgradeDialogFromAccount${upgradeDialogKey}`}
open={upgradeDialogOpen}
onCancel={() => setUpgradeDialogOpen(false)}
/>
}
</div>
2023-01-30 19:10:45 +01:00
{account.billing?.status === SubscriptionStatus.PAST_DUE &&
2023-02-09 21:24:12 +01:00
<Alert severity="error" sx={{mt: 1}}>{t("account_basics_tier_payment_overdue")}</Alert>
}
{account.billing?.cancel_at > 0 &&
2023-02-09 21:24:12 +01:00
<Alert severity="warning" sx={{mt: 1}}>{t("account_basics_tier_canceled_subscription", { date: formatShortDate(account.billing.cancel_at) })}</Alert>
}
2023-02-02 21:19:37 +01:00
<Portal>
<Snackbar
open={showPortalError}
autoHideDuration={3000}
onClose={() => setShowPortalError(false)}
message={t("account_usage_cannot_create_portal_session")}
/>
</Portal>
</Pref>
)
};
const Stats = () => {
const { t } = useTranslation();
const { account } = useContext(AccountContext);
if (!account) {
return <></>;
}
const normalize = (value, max) => {
return Math.min(value / max * 100, 100);
};
2022-12-29 08:32:05 +01:00
return (
<Card sx={{p: 3}} aria-label={t("account_usage_title")}>
<Typography variant="h5" sx={{marginBottom: 2}}>
{t("account_usage_title")}
</Typography>
<PrefGroup>
2023-02-02 21:19:37 +01:00
<Pref title={t("account_usage_reservations_title")}>
{(account.role === Role.ADMIN || account.limits.reservations > 0) &&
<>
<div>
<Typography variant="body2" sx={{float: "left"}}>{account.stats.reservations}</Typography>
<Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", {limit: account.limits.reservations}) : t("account_usage_unlimited")}</Typography>
</div>
<LinearProgress
variant="determinate"
value={account.role === Role.USER && account.limits.reservations > 0 ? normalize(account.stats.reservations, account.limits.reservations) : 100}
/>
</>
}
{account.role === Role.USER && account.limits.reservations === 0 &&
<em>{t("account_usage_reservations_none")}</em>
}
</Pref>
2023-01-05 21:20:44 +01:00
<Pref title={
<>
{t("account_usage_messages_title")}
<Tooltip title={t("account_usage_limits_reset_daily")}><span><InfoIcon/></span></Tooltip>
</>
}>
2022-12-29 08:32:05 +01:00
<div>
<Typography variant="body2" sx={{float: "left"}}>{account.stats.messages}</Typography>
2023-01-30 19:10:45 +01:00
<Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")}</Typography>
2022-12-29 08:32:05 +01:00
</div>
2023-01-05 02:34:22 +01:00
<LinearProgress
variant="determinate"
2023-01-30 19:10:45 +01:00
value={account.role === Role.USER ? normalize(account.stats.messages, account.limits.messages) : 100}
2023-01-05 02:34:22 +01:00
/>
2022-12-29 08:32:05 +01:00
</Pref>
2023-01-05 21:20:44 +01:00
<Pref title={
<>
{t("account_usage_emails_title")}
<Tooltip title={t("account_usage_limits_reset_daily")}><span><InfoIcon/></span></Tooltip>
</>
}>
2022-12-29 08:32:05 +01:00
<div>
<Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography>
2023-01-30 19:10:45 +01:00
<Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}</Typography>
2022-12-29 08:32:05 +01:00
</div>
2023-01-05 02:34:22 +01:00
<LinearProgress
variant="determinate"
2023-01-30 19:10:45 +01:00
value={account.role === Role.USER ? normalize(account.stats.emails, account.limits.emails) : 100}
2023-01-05 02:34:22 +01:00
/>
2022-12-29 08:32:05 +01:00
</Pref>
2023-01-09 21:40:46 +01:00
<Pref
alignTop
title={t("account_usage_attachment_storage_title")}
description={t("account_usage_attachment_storage_description", {
filesize: formatBytes(account.limits.attachment_file_size),
expiry: humanizeDuration(account.limits.attachment_expiry_duration * 1000, {
language: i18n.language,
fallbacks: ["en"]
})
})}
>
2022-12-29 08:32:05 +01:00
<div>
<Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography>
2023-01-30 19:10:45 +01:00
<Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")}</Typography>
2022-12-29 08:32:05 +01:00
</div>
2023-01-05 02:34:22 +01:00
<LinearProgress
variant="determinate"
2023-01-30 19:10:45 +01:00
value={account.role === Role.USER ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100}
2023-01-05 02:34:22 +01:00
/>
2022-12-29 08:32:05 +01:00
</Pref>
</PrefGroup>
2023-01-30 19:10:45 +01:00
{account.role === Role.USER && account.limits.basis === LimitBasis.IP &&
2022-12-29 08:32:05 +01:00
<Typography variant="body1">
2023-01-09 21:40:46 +01:00
{t("account_usage_basis_ip_description")}
2022-12-29 08:32:05 +01:00
</Typography>
}
</Card>
);
};
2023-01-05 21:20:44 +01:00
const InfoIcon = () => {
return (
<InfoOutlinedIcon sx={{
2023-01-16 16:35:12 +01:00
verticalAlign: "middle",
2023-01-05 21:20:44 +01:00
width: "18px",
marginLeft: "4px",
color: "gray"
}}/>
);
}
2023-01-28 05:10:59 +01:00
const Tokens = () => {
const { t } = useTranslation();
const { account } = useContext(AccountContext);
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
const tokens = account?.tokens || [];
const handleCreateClick = () => {
setDialogKey(prev => prev+1);
setDialogOpen(true);
};
const handleDialogClose = () => {
setDialogOpen(false);
};
const handleDialogSubmit = async (user) => {
setDialogOpen(false);
//
};
return (
<Card sx={{ padding: 1 }} aria-label={t("prefs_users_title")}>
<CardContent sx={{ paddingBottom: 1 }}>
<Typography variant="h5" sx={{marginBottom: 2}}>
{t("account_tokens_title")}
</Typography>
<Paragraph>
<Trans
i18nKey="account_tokens_description"
components={{
Link: <Link href="/docs"/>
}}
/>
</Paragraph>
{tokens?.length > 0 && <TokensTable tokens={tokens}/>}
</CardContent>
<CardActions>
<Button onClick={handleCreateClick}>{t("account_tokens_table_create_token_button")}</Button>
</CardActions>
<TokenDialog
key={`tokenDialogCreate${dialogKey}`}
open={dialogOpen}
onClose={handleDialogClose}
/>
</Card>
);
};
const TokensTable = (props) => {
const { t } = useTranslation();
const [snackOpen, setSnackOpen] = useState(false);
const [upsertDialogKey, setUpsertDialogKey] = useState(0);
const [upsertDialogOpen, setUpsertDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [selectedToken, setSelectedToken] = useState(null);
const tokens = (props.tokens || [])
.sort( (a, b) => {
if (a.token === session.token()) {
return -1;
} else if (b.token === session.token()) {
return 1;
}
return a.token.localeCompare(b.token);
});
const handleEditClick = (token) => {
setUpsertDialogKey(prev => prev+1);
setSelectedToken(token);
setUpsertDialogOpen(true);
};
const handleDialogClose = () => {
setUpsertDialogOpen(false);
setDeleteDialogOpen(false);
setSelectedToken(null);
};
const handleDeleteClick = async (token) => {
setSelectedToken(token);
setDeleteDialogOpen(true);
};
const handleCopy = async (token) => {
await navigator.clipboard.writeText(token);
setSnackOpen(true);
};
return (
<Table size="small" aria-label={t("account_tokens_title")}>
<TableHead>
<TableRow>
<TableCell sx={{paddingLeft: 0}}>{t("account_tokens_table_token_header")}</TableCell>
<TableCell>{t("account_tokens_table_label_header")}</TableCell>
<TableCell>{t("account_tokens_table_expires_header")}</TableCell>
2023-01-29 02:29:06 +01:00
<TableCell>{t("account_tokens_table_last_access_header")}</TableCell>
2023-01-28 05:10:59 +01:00
<TableCell/>
</TableRow>
</TableHead>
<TableBody>
{tokens.map(token => (
<TableRow
key={token.token}
2023-01-29 02:29:06 +01:00
sx={{'&:last-child td, &:last-child th': { border: 0 }}}
2023-01-28 05:10:59 +01:00
>
2023-01-29 02:29:06 +01:00
<TableCell component="th" scope="row" sx={{ paddingLeft: 0, whiteSpace: "nowrap" }} aria-label={t("account_tokens_table_token_header")}>
2023-01-28 05:10:59 +01:00
<span>
2023-01-29 02:29:06 +01:00
<span style={{fontFamily: "Monospace", fontSize: "0.9rem"}}>{token.token.slice(0, 12)}</span>
2023-01-28 05:10:59 +01:00
...
<Tooltip title={t("account_tokens_table_copy_to_clipboard")} placement="right">
<IconButton onClick={() => handleCopy(token.token)}><ContentCopy/></IconButton>
</Tooltip>
</span>
</TableCell>
<TableCell aria-label={t("account_tokens_table_label_header")}>
{token.token === session.token() && <em>{t("account_tokens_table_current_session")}</em>}
{token.token !== session.token() && (token.label || "-")}
</TableCell>
2023-01-29 04:13:43 +01:00
<TableCell sx={{ whiteSpace: "nowrap" }} aria-label={t("account_tokens_table_expires_header")}>
2023-01-28 05:10:59 +01:00
{token.expires ? formatShortDateTime(token.expires) : <em>{t("account_tokens_table_never_expires")}</em>}
</TableCell>
2023-01-29 04:13:43 +01:00
<TableCell sx={{ whiteSpace: "nowrap" }} aria-label={t("account_tokens_table_last_access_header")}>
2023-01-29 02:29:06 +01:00
<div style={{ display: "flex", alignItems: "center" }}>
<span>{formatShortDateTime(token.last_access)}</span>
<Tooltip title={t("account_tokens_table_last_origin_tooltip", { ip: token.last_origin })}>
<IconButton onClick={() => openUrl(`https://whatismyipaddress.com/ip/${token.last_origin}`)}>
<Public />
</IconButton>
</Tooltip>
</div>
</TableCell>
<TableCell align="right" sx={{ whiteSpace: "nowrap" }}>
2023-01-28 05:10:59 +01:00
{token.token !== session.token() &&
<>
<IconButton onClick={() => handleEditClick(token)} aria-label={t("account_tokens_dialog_title_edit")}>
<EditIcon/>
</IconButton>
<IconButton onClick={() => handleDeleteClick(token)} aria-label={t("account_tokens_dialog_title_delete")}>
<CloseIcon/>
</IconButton>
</>
}
{token.token === session.token() &&
<Tooltip title={t("account_tokens_table_cannot_delete_or_edit")}>
<span>
<IconButton disabled><EditIcon/></IconButton>
<IconButton disabled><CloseIcon/></IconButton>
</span>
</Tooltip>
}
</TableCell>
</TableRow>
))}
</TableBody>
<Portal>
<Snackbar
open={snackOpen}
autoHideDuration={3000}
onClose={() => setSnackOpen(false)}
message={t("account_tokens_table_copied_to_clipboard")}
/>
</Portal>
<TokenDialog
key={`tokenDialogEdit${upsertDialogKey}`}
open={upsertDialogOpen}
token={selectedToken}
onClose={handleDialogClose}
/>
<TokenDeleteDialog
open={deleteDialogOpen}
token={selectedToken}
onClose={handleDialogClose}
/>
</Table>
);
};
const TokenDialog = (props) => {
const { t } = useTranslation();
2023-02-02 21:19:37 +01:00
const [error, setError] = useState("");
2023-01-28 05:10:59 +01:00
const [label, setLabel] = useState(props.token?.label || "");
const [expires, setExpires] = useState(props.token ? -1 : 0);
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const editMode = !!props.token;
const handleSubmit = async () => {
try {
if (editMode) {
await accountApi.updateToken(props.token.token, label, expires);
} else {
await accountApi.createToken(label, expires);
}
props.onClose();
} catch (e) {
console.log(`[Account] Error creating token`, e);
2023-02-02 21:19:37 +01:00
if (e instanceof UnauthorizedError) {
2023-01-28 05:10:59 +01:00
session.resetAndRedirect(routes.login);
2023-02-02 21:19:37 +01:00
} else {
setError(e.message);
2023-01-28 05:10:59 +01:00
}
}
};
return (
<Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}>
<DialogTitle>{editMode ? t("account_tokens_dialog_title_edit") : t("account_tokens_dialog_title_create")}</DialogTitle>
<DialogContent>
<TextField
margin="dense"
id="token-label"
label={t("account_tokens_dialog_label")}
aria-label={t("account_delete_dialog_label")}
type="text"
value={label}
onChange={ev => setLabel(ev.target.value)}
fullWidth
variant="standard"
/>
<FormControl fullWidth variant="standard" sx={{ mt: 1 }}>
<Select value={expires} onChange={(ev) => setExpires(ev.target.value)} aria-label={t("account_tokens_dialog_expires_label")}>
{editMode && <MenuItem value={-1}>{t("account_tokens_dialog_expires_unchanged")}</MenuItem>}
<MenuItem value={0}>{t("account_tokens_dialog_expires_never")}</MenuItem>
<MenuItem value={21600}>{t("account_tokens_dialog_expires_x_hours", { hours: 6 })}</MenuItem>
<MenuItem value={43200}>{t("account_tokens_dialog_expires_x_hours", { hours: 12 })}</MenuItem>
<MenuItem value={259200}>{t("account_tokens_dialog_expires_x_days", { days: 3 })}</MenuItem>
<MenuItem value={604800}>{t("account_tokens_dialog_expires_x_days", { days: 7 })}</MenuItem>
<MenuItem value={2592000}>{t("account_tokens_dialog_expires_x_days", { days: 30 })}</MenuItem>
<MenuItem value={7776000}>{t("account_tokens_dialog_expires_x_days", { days: 90 })}</MenuItem>
<MenuItem value={15552000}>{t("account_tokens_dialog_expires_x_days", { days: 180 })}</MenuItem>
</Select>
</FormControl>
</DialogContent>
2023-02-02 21:19:37 +01:00
<DialogFooter status={error}>
2023-01-28 05:10:59 +01:00
<Button onClick={props.onClose}>{t("account_tokens_dialog_button_cancel")}</Button>
<Button onClick={handleSubmit}>{editMode ? t("account_tokens_dialog_button_update") : t("account_tokens_dialog_button_create")}</Button>
</DialogFooter>
</Dialog>
);
};
const TokenDeleteDialog = (props) => {
const { t } = useTranslation();
2023-02-02 21:19:37 +01:00
const [error, setError] = useState("");
2023-01-28 05:10:59 +01:00
const handleSubmit = async () => {
try {
await accountApi.deleteToken(props.token.token);
props.onClose();
} catch (e) {
console.log(`[Account] Error deleting token`, e);
2023-02-02 21:19:37 +01:00
if (e instanceof UnauthorizedError) {
2023-01-28 05:10:59 +01:00
session.resetAndRedirect(routes.login);
2023-02-02 21:19:37 +01:00
} else {
setError(e.message);
2023-01-28 05:10:59 +01:00
}
}
};
return (
<Dialog open={props.open} onClose={props.onClose}>
<DialogTitle>{t("account_tokens_delete_dialog_title")}</DialogTitle>
<DialogContent>
<DialogContentText>
<Trans i18nKey="account_tokens_delete_dialog_description"/>
</DialogContentText>
</DialogContent>
2023-02-02 21:19:37 +01:00
<DialogFooter status>
2023-01-28 05:10:59 +01:00
<Button onClick={props.onClose}>{t("common_cancel")}</Button>
<Button onClick={handleSubmit} color="error">{t("account_tokens_delete_dialog_submit_button")}</Button>
2023-02-02 21:19:37 +01:00
</DialogFooter>
2023-01-28 05:10:59 +01:00
</Dialog>
);
}
2022-12-29 08:32:05 +01:00
const Delete = () => {
const { t } = useTranslation();
return (
<Card sx={{p: 3}} aria-label={t("account_delete_title")}>
<Typography variant="h5" sx={{marginBottom: 2}}>
{t("account_delete_title")}
</Typography>
<PrefGroup>
<DeleteAccount/>
</PrefGroup>
</Card>
);
};
2022-12-16 04:07:04 +01:00
const DeleteAccount = () => {
const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
2022-12-29 08:32:05 +01:00
2022-12-16 04:07:04 +01:00
const handleDialogOpen = () => {
setDialogKey(prev => prev+1);
setDialogOpen(true);
};
2022-12-29 08:32:05 +01:00
const handleDialogClose = () => {
2022-12-16 04:07:04 +01:00
setDialogOpen(false);
};
2022-12-29 08:32:05 +01:00
2022-12-16 04:07:04 +01:00
return (
2022-12-29 08:32:05 +01:00
<Pref title={t("account_delete_title")} description={t("account_delete_description")}>
2022-12-17 19:49:32 +01:00
<div>
<Button fullWidth={false} variant="outlined" color="error" startIcon={<DeleteOutlineIcon />} onClick={handleDialogOpen}>
2022-12-29 08:32:05 +01:00
{t("account_delete_title")}
2022-12-17 19:49:32 +01:00
</Button>
</div>
2022-12-16 04:07:04 +01:00
<DeleteAccountDialog
key={`deleteAccountDialog${dialogKey}`}
open={dialogOpen}
onClose={handleDialogClose}
2022-12-16 04:07:04 +01:00
/>
</Pref>
)
};
const DeleteAccountDialog = (props) => {
const { t } = useTranslation();
const { account } = useContext(AccountContext);
2023-02-02 21:19:37 +01:00
const [error, setError] = useState("");
const [password, setPassword] = useState("");
2022-12-16 04:07:04 +01:00
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const handleSubmit = async () => {
try {
await accountApi.delete(password);
await db.delete();
console.debug(`[Account] Account deleted`);
session.resetAndRedirect(routes.app);
} catch (e) {
console.log(`[Account] Error deleting account`, e);
2023-02-02 21:19:37 +01:00
if (e instanceof IncorrectPasswordError) {
setError(t("account_basics_password_dialog_current_password_incorrect"));
} else if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
2023-02-02 21:19:37 +01:00
} else {
setError(e.message);
}
}
};
2022-12-16 04:07:04 +01:00
return (
<Dialog open={props.open} onClose={props.onClose} fullScreen={fullScreen}>
2022-12-29 08:32:05 +01:00
<DialogTitle>{t("account_delete_title")}</DialogTitle>
2022-12-16 04:07:04 +01:00
<DialogContent>
<Typography variant="body1">
{t("account_delete_dialog_description")}
2022-12-16 04:07:04 +01:00
</Typography>
<TextField
margin="dense"
id="account-delete-confirm"
label={t("account_delete_dialog_label")}
aria-label={t("account_delete_dialog_label")}
type="password"
value={password}
onChange={ev => setPassword(ev.target.value)}
2022-12-16 04:07:04 +01:00
fullWidth
variant="standard"
/>
{account?.billing?.subscription &&
<Alert severity="warning" sx={{mt: 1}}>{t("account_delete_dialog_billing_warning")}</Alert>
}
2022-12-16 04:07:04 +01:00
</DialogContent>
2023-02-02 21:19:37 +01:00
<DialogFooter status={error}>
<Button onClick={props.onClose}>{t("account_delete_dialog_button_cancel")}</Button>
<Button onClick={handleSubmit} color="error" disabled={password.length === 0}>{t("account_delete_dialog_button_submit")}</Button>
</DialogFooter>
2022-12-16 04:07:04 +01:00
</Dialog>
);
};
export default Account;