JS error handling

This commit is contained in:
binwiederhier 2023-02-02 15:19:37 -05:00
parent 180a7df1e7
commit 0885951a67
20 changed files with 369 additions and 366 deletions

View file

@ -1,12 +1,19 @@
import * as React from 'react';
import {useContext, useEffect, useState} from 'react';
import {useContext, useState} from 'react';
import {
Alert,
CardActions,
CardContent, FormControl,
LinearProgress, Link, Portal, Select, Snackbar,
CardContent,
FormControl,
LinearProgress,
Link,
Portal,
Select,
Snackbar,
Stack,
Table, TableBody, TableCell,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
useMediaQuery
@ -27,14 +34,8 @@ import DialogContent from "@mui/material/DialogContent";
import TextField from "@mui/material/TextField";
import routes from "./routes";
import IconButton from "@mui/material/IconButton";
import {formatBytes, formatShortDate, formatShortDateTime, openUrl, truncateString, validUrl} from "../app/utils";
import accountApi, {
IncorrectPasswordError,
LimitBasis,
Role,
SubscriptionStatus,
UnauthorizedError
} from "../app/AccountApi";
import {formatBytes, formatShortDate, formatShortDateTime, openUrl} from "../app/utils";
import accountApi, {LimitBasis, Role, SubscriptionStatus} from "../app/AccountApi";
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import {Pref, PrefGroup} from "./Pref";
import db from "../app/db";
@ -44,17 +45,12 @@ import UpgradeDialog from "./UpgradeDialog";
import CelebrationIcon from "@mui/icons-material/Celebration";
import {AccountContext} from "./App";
import DialogFooter from "./DialogFooter";
import {useLiveQuery} from "dexie-react-hooks";
import userManager from "../app/UserManager";
import {Paragraph} from "./styles";
import CloseIcon from "@mui/icons-material/Close";
import DialogActions from "@mui/material/DialogActions";
import {ContentCopy, Public} from "@mui/icons-material";
import MenuItem from "@mui/material/MenuItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons";
import ListItemText from "@mui/material/ListItemText";
import DialogContentText from "@mui/material/DialogContentText";
import {IncorrectPasswordError, UnauthorizedError} from "../app/errors";
const Account = () => {
if (!session.exists()) {
@ -140,11 +136,10 @@ const ChangePassword = () => {
const ChangePasswordDialog = (props) => {
const { t } = useTranslation();
const [error, setError] = useState("");
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [errorText, setErrorText] = useState("");
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const handleDialogSubmit = async () => {
@ -154,12 +149,13 @@ const ChangePasswordDialog = (props) => {
props.onClose();
} catch (e) {
console.log(`[Account] Error changing password`, e);
if ((e instanceof IncorrectPasswordError)) {
setErrorText(t("account_basics_password_dialog_current_password_incorrect"));
} else if ((e instanceof UnauthorizedError)) {
if (e instanceof IncorrectPasswordError) {
setError(t("account_basics_password_dialog_current_password_incorrect"));
} else if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setError(e.message);
}
// TODO show error
}
};
@ -201,7 +197,7 @@ const ChangePasswordDialog = (props) => {
variant="standard"
/>
</DialogContent>
<DialogFooter status={errorText}>
<DialogFooter status={error}>
<Button onClick={props.onClose}>{t("account_basics_password_dialog_button_cancel")}</Button>
<Button
onClick={handleDialogSubmit}
@ -219,6 +215,7 @@ const AccountType = () => {
const { account } = useContext(AccountContext);
const [upgradeDialogKey, setUpgradeDialogKey] = useState(0);
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
const [showPortalError, setShowPortalError] = useState(false);
if (!account) {
return <></>;
@ -234,11 +231,12 @@ const AccountType = () => {
const response = await accountApi.createBillingPortalSession();
window.open(response.redirect_url, "billing_portal");
} catch (e) {
console.log(`[Account] Error changing password`, e);
if ((e instanceof UnauthorizedError)) {
console.log(`[Account] Error opening billing portal`, e);
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setShowPortalError(true);
}
// TODO show error
}
};
@ -302,6 +300,14 @@ const AccountType = () => {
{account.billing?.cancel_at > 0 &&
<Alert severity="warning" sx={{mt: 1}}>{t("account_usage_tier_canceled_subscription", { date: formatShortDate(account.billing.cancel_at) })}</Alert>
}
<Portal>
<Snackbar
open={showPortalError}
autoHideDuration={3000}
onClose={() => setShowPortalError(false)}
message={t("account_usage_cannot_create_portal_session")}
/>
</Portal>
</Pref>
)
};
@ -324,27 +330,23 @@ const Stats = () => {
{t("account_usage_title")}
</Typography>
<PrefGroup>
{account.role === Role.USER &&
<Pref title={t("account_usage_reservations_title")}>
{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.limits.reservations > 0 ? normalize(account.stats.reservations, account.limits.reservations) : 100}
/>
</>
}
{account.limits.reservations === 0 &&
<em>No reserved topics for this account</em>
}
</Pref>
}
<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>
<Pref title={
<>
{t("account_usage_messages_title")}
@ -596,9 +598,9 @@ const TokensTable = (props) => {
const TokenDialog = (props) => {
const { t } = useTranslation();
const [error, setError] = useState("");
const [label, setLabel] = useState(props.token?.label || "");
const [expires, setExpires] = useState(props.token ? -1 : 0);
const [errorText, setErrorText] = useState("");
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const editMode = !!props.token;
@ -612,10 +614,11 @@ const TokenDialog = (props) => {
props.onClose();
} catch (e) {
console.log(`[Account] Error creating token`, e);
if ((e instanceof UnauthorizedError)) {
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setError(e.message);
}
// TODO show error
}
};
@ -648,7 +651,7 @@ const TokenDialog = (props) => {
</Select>
</FormControl>
</DialogContent>
<DialogFooter status={errorText}>
<DialogFooter status={error}>
<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>
@ -658,6 +661,7 @@ const TokenDialog = (props) => {
const TokenDeleteDialog = (props) => {
const { t } = useTranslation();
const [error, setError] = useState("");
const handleSubmit = async () => {
try {
@ -665,10 +669,11 @@ const TokenDeleteDialog = (props) => {
props.onClose();
} catch (e) {
console.log(`[Account] Error deleting token`, e);
if ((e instanceof UnauthorizedError)) {
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setError(e.message);
}
// TODO show error
}
};
@ -680,10 +685,10 @@ const TokenDeleteDialog = (props) => {
<Trans i18nKey="account_tokens_delete_dialog_description"/>
</DialogContentText>
</DialogContent>
<DialogActions>
<DialogFooter status>
<Button onClick={props.onClose}>{t("common_cancel")}</Button>
<Button onClick={handleSubmit} color="error">{t("account_tokens_delete_dialog_submit_button")}</Button>
</DialogActions>
</DialogFooter>
</Dialog>
);
}
@ -736,8 +741,8 @@ const DeleteAccount = () => {
const DeleteAccountDialog = (props) => {
const { t } = useTranslation();
const { account } = useContext(AccountContext);
const [error, setError] = useState("");
const [password, setPassword] = useState("");
const [errorText, setErrorText] = useState("");
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const handleSubmit = async () => {
@ -748,12 +753,13 @@ const DeleteAccountDialog = (props) => {
session.resetAndRedirect(routes.app);
} catch (e) {
console.log(`[Account] Error deleting account`, e);
if ((e instanceof IncorrectPasswordError)) {
setErrorText(t("account_basics_password_dialog_current_password_incorrect"));
} else if ((e instanceof UnauthorizedError)) {
if (e instanceof IncorrectPasswordError) {
setError(t("account_basics_password_dialog_current_password_incorrect"));
} else if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setError(e.message);
}
// TODO show error
}
};
@ -779,7 +785,7 @@ const DeleteAccountDialog = (props) => {
<Alert severity="warning" sx={{mt: 1}}>{t("account_delete_dialog_billing_warning")}</Alert>
}
</DialogContent>
<DialogFooter status={errorText}>
<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>

View file

@ -10,10 +10,11 @@ import session from "../app/Session";
import {NavLink} from "react-router-dom";
import AvatarBox from "./AvatarBox";
import {useTranslation} from "react-i18next";
import accountApi, {UnauthorizedError} from "../app/AccountApi";
import accountApi from "../app/AccountApi";
import IconButton from "@mui/material/IconButton";
import {InputAdornment} from "@mui/material";
import {Visibility, VisibilityOff} from "@mui/icons-material";
import {UnauthorizedError} from "../app/errors";
const Login = () => {
const { t } = useTranslation();
@ -32,12 +33,10 @@ const Login = () => {
window.location.href = routes.app;
} catch (e) {
console.log(`[Login] User auth for user ${user.username} failed`, e);
if ((e instanceof UnauthorizedError)) {
if (e instanceof UnauthorizedError) {
setError(t("Login failed: Invalid username or password"));
} else if (e.message) {
setError(e.message);
} else {
setError(t("Unknown error. Check logs for details."))
setError(e.message);
}
}
};

View file

@ -39,13 +39,16 @@ import {playSound, shuffle, sounds, validUrl} from "../app/utils";
import {useTranslation} from "react-i18next";
import session from "../app/Session";
import routes from "./routes";
import accountApi, {Permission, Role, UnauthorizedError} from "../app/AccountApi";
import accountApi, {Permission, Role} from "../app/AccountApi";
import {Pref, PrefGroup} from "./Pref";
import {Info} from "@mui/icons-material";
import {AccountContext} from "./App";
import {useOutletContext} from "react-router-dom";
import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons";
import {ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog} from "./ReserveDialogs";
import {UnauthorizedError} from "../app/errors";
import subscriptionManager from "../app/SubscriptionManager";
import {subscribeTopic} from "./SubscribeDialog";
const Preferences = () => {
return (
@ -484,7 +487,7 @@ const Reservations = () => {
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
if (!config.enable_reservations || !session.exists() || !account || account.role === Role.ADMIN) {
if (!config.enable_reservations || !session.exists() || !account) {
return <></>;
}
const reservations = account.reservations || [];
@ -543,6 +546,10 @@ const ReservationsTable = (props) => {
setDeleteDialogOpen(true);
};
const handleSubscribeClick = async (reservation) => {
await subscribeTopic(config.base_url, reservation.topic);
};
return (
<Table size="small" aria-label={t("prefs_reservations_table")}>
<TableHead>
@ -589,7 +596,9 @@ const ReservationsTable = (props) => {
</TableCell>
<TableCell align="right" sx={{ whiteSpace: "nowrap" }}>
{!localSubscriptions[reservation.topic] &&
<Chip icon={<Info/>} label={t("prefs_reservations_table_not_subscribed")} color="primary" variant="outlined"/>
<Tooltip title={t("prefs_reservations_table_click_to_subscribe")}>
<Chip icon={<Info/>} onClick={() => handleSubscribeClick(reservation)} label={t("prefs_reservations_table_not_subscribed")} color="primary" variant="outlined"/>
</Tooltip>
}
<IconButton onClick={() => handleEditClick(reservation)} aria-label={t("prefs_reservations_edit_button")}>
<EditIcon/>
@ -626,7 +635,7 @@ const maybeUpdateAccountSettings = async (payload) => {
await accountApi.updateSettings(payload);
} catch (e) {
console.log(`[Preferences] Error updating account settings`, e);
if ((e instanceof UnauthorizedError)) {
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
}
}

View file

@ -27,7 +27,8 @@ import EmojiPicker from "./EmojiPicker";
import {Trans, useTranslation} from "react-i18next";
import session from "../app/Session";
import routes from "./routes";
import accountApi, {UnauthorizedError} from "../app/AccountApi";
import accountApi from "../app/AccountApi";
import {UnauthorizedError} from "../app/errors";
const PublishDialog = (props) => {
const { t } = useTranslation();
@ -179,7 +180,7 @@ const PublishDialog = (props) => {
setAttachFileError("");
} catch (e) {
console.log(`[PublishDialog] Retrieving attachment limits failed`, e);
if ((e instanceof UnauthorizedError)) {
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setAttachFileError(""); // Reset error (rely on server-side checking)

View file

@ -1,46 +1,31 @@
import * as React from 'react';
import {useContext, useEffect, useState} from 'react';
import {useState} from 'react';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import {
Alert,
Autocomplete,
Checkbox,
FormControl,
FormControlLabel,
FormGroup,
Select,
useMediaQuery
} from "@mui/material";
import {Alert, FormControl, Select, useMediaQuery} from "@mui/material";
import theme from "./theme";
import api from "../app/Api";
import {randomAlphanumericString, topicUrl, validTopic, validUrl} from "../app/utils";
import userManager from "../app/UserManager";
import subscriptionManager from "../app/SubscriptionManager";
import poller from "../app/Poller";
import {validTopic} from "../app/utils";
import DialogFooter from "./DialogFooter";
import {useTranslation} from "react-i18next";
import session from "../app/Session";
import routes from "./routes";
import accountApi, {Permission, Role, TopicReservedError, UnauthorizedError} from "../app/AccountApi";
import accountApi, {Permission} from "../app/AccountApi";
import ReserveTopicSelect from "./ReserveTopicSelect";
import {AccountContext} from "./App";
import DialogActions from "@mui/material/DialogActions";
import MenuItem from "@mui/material/MenuItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons";
import ListItemText from "@mui/material/ListItemText";
import {Check, DeleteForever} from "@mui/icons-material";
import {TopicReservedError, UnauthorizedError} from "../app/errors";
export const ReserveAddDialog = (props) => {
const { t } = useTranslation();
const [error, setError] = useState("");
const [topic, setTopic] = useState(props.topic || "");
const [everyone, setEveryone] = useState(Permission.DENY_ALL);
const [errorText, setErrorText] = useState("");
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const allowTopicEdit = !props.topic;
const alreadyReserved = props.reservations.filter(r => r.topic === topic).length > 0;
@ -52,15 +37,17 @@ export const ReserveAddDialog = (props) => {
console.debug(`[ReserveAddDialog] Added reservation for topic ${t}: ${everyone}`);
} catch (e) {
console.log(`[ReserveAddDialog] Error adding topic reservation.`, e);
if ((e instanceof UnauthorizedError)) {
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else if ((e instanceof TopicReservedError)) {
setErrorText(t("subscribe_dialog_error_topic_already_reserved"));
} else if (e instanceof TopicReservedError) {
setError(t("subscribe_dialog_error_topic_already_reserved"));
return;
} else {
setError(e.message);
return;
}
}
props.onClose();
// FIXME handle 401/403/409
};
return (
@ -88,7 +75,7 @@ export const ReserveAddDialog = (props) => {
sx={{mt: 1}}
/>
</DialogContent>
<DialogFooter status={errorText}>
<DialogFooter status={error}>
<Button onClick={props.onClose}>{t("prefs_users_dialog_button_cancel")}</Button>
<Button onClick={handleSubmit} disabled={!submitButtonEnabled}>{t("prefs_users_dialog_button_add")}</Button>
</DialogFooter>
@ -98,6 +85,7 @@ export const ReserveAddDialog = (props) => {
export const ReserveEditDialog = (props) => {
const { t } = useTranslation();
const [error, setError] = useState("");
const [everyone, setEveryone] = useState(props.reservation?.everyone || Permission.DENY_ALL);
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
@ -107,12 +95,14 @@ export const ReserveEditDialog = (props) => {
console.debug(`[ReserveEditDialog] Updated reservation for topic ${t}: ${everyone}`);
} catch (e) {
console.log(`[ReserveEditDialog] Error updating topic reservation.`, e);
if ((e instanceof UnauthorizedError)) {
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setError(e.message);
return;
}
}
props.onClose();
// FIXME handle 401/403/409
};
return (
@ -128,31 +118,34 @@ export const ReserveEditDialog = (props) => {
sx={{mt: 1}}
/>
</DialogContent>
<DialogActions>
<DialogFooter status={error}>
<Button onClick={props.onClose}>{t("common_cancel")}</Button>
<Button onClick={handleSubmit}>{t("common_save")}</Button>
</DialogActions>
</DialogFooter>
</Dialog>
);
};
export const ReserveDeleteDialog = (props) => {
const { t } = useTranslation();
const [error, setError] = useState("");
const [deleteMessages, setDeleteMessages] = useState(false);
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const handleSubmit = async () => {
try {
await accountApi.deleteReservation(props.topic, deleteMessages);
console.debug(`[ReserveDeleteDialog] Deleted reservation for topic ${t}`);
console.debug(`[ReserveDeleteDialog] Deleted reservation for topic ${props.topic}`);
} catch (e) {
console.log(`[ReserveDeleteDialog] Error deleting topic reservation.`, e);
if ((e instanceof UnauthorizedError)) {
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setError(e.message);
return;
}
}
props.onClose();
// FIXME handle 401/403/409
};
return (
@ -196,10 +189,10 @@ export const ReserveDeleteDialog = (props) => {
</Alert>
}
</DialogContent>
<DialogActions>
<DialogFooter status={error}>
<Button onClick={props.onClose}>{t("common_cancel")}</Button>
<Button onClick={handleSubmit} color="error">{t("reservation_delete_dialog_submit_button")}</Button>
</DialogActions>
</DialogFooter>
</Dialog>
);
};

View file

@ -10,10 +10,11 @@ import {NavLink} from "react-router-dom";
import AvatarBox from "./AvatarBox";
import {useTranslation} from "react-i18next";
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
import accountApi, {AccountCreateLimitReachedError, UsernameTakenError} from "../app/AccountApi";
import accountApi from "../app/AccountApi";
import {InputAdornment} from "@mui/material";
import IconButton from "@mui/material/IconButton";
import {Visibility, VisibilityOff} from "@mui/icons-material";
import {AccountCreateLimitReachedError, UserExistsError} from "../app/errors";
const Signup = () => {
const { t } = useTranslation();
@ -35,14 +36,12 @@ const Signup = () => {
window.location.href = routes.app;
} catch (e) {
console.log(`[Signup] Signup for user ${user.username} failed`, e);
if ((e instanceof UsernameTakenError)) {
if (e instanceof UserExistsError) {
setError(t("signup_error_username_taken", { username: e.username }));
} else if ((e instanceof AccountCreateLimitReachedError)) {
setError(t("signup_error_creation_limit_reached"));
} else if (e.message) {
setError(e.message);
} else {
setError(t("signup_error_unknown"))
setError(e.message);
}
}
};

View file

@ -17,9 +17,10 @@ import DialogFooter from "./DialogFooter";
import {useTranslation} from "react-i18next";
import session from "../app/Session";
import routes from "./routes";
import accountApi, {Role, TopicReservedError, UnauthorizedError} from "../app/AccountApi";
import accountApi, {Role} from "../app/AccountApi";
import ReserveTopicSelect from "./ReserveTopicSelect";
import {AccountContext} from "./App";
import {TopicReservedError, UnauthorizedError} from "../app/errors";
const publicBaseUrl = "https://ntfy.sh";
@ -32,22 +33,7 @@ const SubscribeDialog = (props) => {
const handleSuccess = async () => {
console.log(`[SubscribeDialog] Subscribing to topic ${topic}`);
const actualBaseUrl = (baseUrl) ? baseUrl : config.base_url;
const subscription = await subscriptionManager.add(actualBaseUrl, topic);
if (session.exists()) {
try {
const remoteSubscription = await accountApi.addSubscription({
base_url: actualBaseUrl,
topic: topic
});
await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
await accountApi.sync();
} catch (e) {
console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e);
if ((e instanceof UnauthorizedError)) {
session.resetAndRedirect(routes.login);
}
}
}
const subscription = subscribeTopic(actualBaseUrl, topic);
poller.pollInBackground(subscription); // Dangle!
props.onSuccess(subscription);
}
@ -77,9 +63,9 @@ const SubscribeDialog = (props) => {
const SubscribePage = (props) => {
const { t } = useTranslation();
const { account } = useContext(AccountContext);
const [error, setError] = useState("");
const [reserveTopicVisible, setReserveTopicVisible] = useState(false);
const [anotherServerVisible, setAnotherServerVisible] = useState(false);
const [errorText, setErrorText] = useState("");
const [everyone, setEveryone] = useState("deny-all");
const baseUrl = (anotherServerVisible) ? props.baseUrl : config.base_url;
const topic = props.topic;
@ -98,7 +84,7 @@ const SubscribePage = (props) => {
if (!success) {
console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
if (user) {
setErrorText(t("subscribe_dialog_error_user_not_authorized", { username: username }));
setError(t("subscribe_dialog_error_user_not_authorized", { username: username }));
return;
} else {
props.onNeedsLogin();
@ -114,10 +100,10 @@ const SubscribePage = (props) => {
// Account sync later after it was added
} catch (e) {
console.log(`[SubscribeDialog] Error reserving topic`, e);
if ((e instanceof UnauthorizedError)) {
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else if ((e instanceof TopicReservedError)) {
setErrorText(t("subscribe_dialog_error_topic_already_reserved"));
} else if (e instanceof TopicReservedError) {
setError(t("subscribe_dialog_error_topic_already_reserved"));
return;
}
}
@ -231,7 +217,7 @@ const SubscribePage = (props) => {
</FormGroup>
}
</DialogContent>
<DialogFooter status={errorText}>
<DialogFooter status={error}>
<Button onClick={props.onCancel}>{t("subscribe_dialog_subscribe_button_cancel")}</Button>
<Button onClick={handleSubscribe} disabled={!subscribeButtonEnabled}>{t("subscribe_dialog_subscribe_button_subscribe")}</Button>
</DialogFooter>
@ -243,21 +229,23 @@ const LoginPage = (props) => {
const { t } = useTranslation();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [errorText, setErrorText] = useState("");
const [error, setError] = useState("");
const baseUrl = (props.baseUrl) ? props.baseUrl : config.base_url;
const topic = props.topic;
const handleLogin = async () => {
const user = {baseUrl, username, password};
const success = await api.topicAuth(baseUrl, topic, user);
if (!success) {
console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
setErrorText(t("subscribe_dialog_error_user_not_authorized", { username: username }));
setError(t("subscribe_dialog_error_user_not_authorized", { username: username }));
return;
}
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
await userManager.save(user);
props.onSuccess();
};
return (
<>
<DialogTitle>{t("subscribe_dialog_login_title")}</DialogTitle>
@ -293,7 +281,7 @@ const LoginPage = (props) => {
}}
/>
</DialogContent>
<DialogFooter status={errorText}>
<DialogFooter status={error}>
<Button onClick={props.onBack}>{t("subscribe_dialog_login_button_back")}</Button>
<Button onClick={handleLogin}>{t("subscribe_dialog_login_button_login")}</Button>
</DialogFooter>
@ -301,4 +289,23 @@ const LoginPage = (props) => {
);
};
export const subscribeTopic = async (baseUrl, topic) => {
const subscription = await subscriptionManager.add(baseUrl, topic);
if (session.exists()) {
try {
const remoteSubscription = await accountApi.addSubscription({
base_url: baseUrl,
topic: topic
});
await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
} catch (e) {
console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e);
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
}
}
}
return subscription;
};
export default SubscribeDialog;

View file

@ -11,10 +11,9 @@ import theme from "./theme";
import subscriptionManager from "../app/SubscriptionManager";
import DialogFooter from "./DialogFooter";
import {useTranslation} from "react-i18next";
import accountApi, {Permission, UnauthorizedError} from "../app/AccountApi";
import accountApi from "../app/AccountApi";
import session from "../app/Session";
import routes from "./routes";
import ReserveTopicSelect from "./ReserveTopicSelect";
import MenuItem from "@mui/material/MenuItem";
import PopupMenu from "./PopupMenu";
import {formatShortDateTime, shuffle} from "../app/utils";
@ -23,7 +22,8 @@ import {useNavigate} from "react-router-dom";
import IconButton from "@mui/material/IconButton";
import {Clear} from "@mui/icons-material";
import {AccountContext} from "./App";
import {ReserveEditDialog, ReserveAddDialog, ReserveDeleteDialog} from "./ReserveDialogs";
import {ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog} from "./ReserveDialogs";
import {UnauthorizedError} from "../app/errors";
const SubscriptionPopup = (props) => {
const { t } = useTranslation();
@ -96,25 +96,25 @@ const SubscriptionPopup = (props) => {
tags: tags
});
} catch (e) {
console.log(`[ActionBar] Error publishing message`, e);
console.log(`[SubscriptionPopup] Error publishing message`, e);
setShowPublishError(true);
}
}
const handleClearAll = async () => {
console.log(`[ActionBar] Deleting all notifications from ${props.subscription.id}`);
console.log(`[SubscriptionPopup] Deleting all notifications from ${props.subscription.id}`);
await subscriptionManager.deleteNotifications(props.subscription.id);
};
const handleUnsubscribe = async (event) => {
console.log(`[ActionBar] Unsubscribing from ${props.subscription.id}`, props.subscription);
const handleUnsubscribe = async () => {
console.log(`[SubscriptionPopup] Unsubscribing from ${props.subscription.id}`, props.subscription);
await subscriptionManager.remove(props.subscription.id);
if (session.exists() && props.subscription.remoteId) {
try {
await accountApi.deleteSubscription(props.subscription.remoteId);
} catch (e) {
console.log(`[ActionBar] Error unsubscribing`, e);
if ((e instanceof UnauthorizedError)) {
console.log(`[SubscriptionPopup] Error unsubscribing`, e);
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
}
}
@ -187,25 +187,24 @@ const SubscriptionPopup = (props) => {
const DisplayNameDialog = (props) => {
const { t } = useTranslation();
const subscription = props.subscription;
const [error, setError] = useState("");
const [displayName, setDisplayName] = useState(subscription.displayName ?? "");
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const handleSave = async () => {
// Apply locally
await subscriptionManager.setDisplayName(subscription.id, displayName);
// Apply remotely
if (session.exists() && subscription.remoteId) {
try {
console.log(`[SubscriptionSettingsDialog] Updating subscription display name to ${displayName}`);
await accountApi.updateSubscription(subscription.remoteId, { display_name: displayName });
} catch (e) {
console.log(`[SubscriptionSettingsDialog] Error updating subscription`, e);
if ((e instanceof UnauthorizedError)) {
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setError(e.message);
return;
}
// FIXME handle 409
}
}
props.onClose();
@ -241,7 +240,7 @@ const DisplayNameDialog = (props) => {
}}
/>
</DialogContent>
<DialogFooter>
<DialogFooter status={error}>
<Button onClick={props.onClose}>{t("common_cancel")}</Button>
<Button onClick={handleSave}>{t("common_save")}</Button>
</DialogFooter>

View file

@ -7,7 +7,7 @@ import {Alert, CardActionArea, CardContent, ListItem, useMediaQuery} from "@mui/
import theme from "./theme";
import DialogFooter from "./DialogFooter";
import Button from "@mui/material/Button";
import accountApi, {UnauthorizedError} from "../app/AccountApi";
import accountApi from "../app/AccountApi";
import session from "../app/Session";
import routes from "./routes";
import Card from "@mui/material/Card";
@ -21,19 +21,24 @@ import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Box from "@mui/material/Box";
import {NavLink} from "react-router-dom";
import {UnauthorizedError} from "../app/errors";
const UpgradeDialog = (props) => {
const { t } = useTranslation();
const { account } = useContext(AccountContext); // May be undefined!
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const [error, setError] = useState("");
const [tiers, setTiers] = useState(null);
const [newTierCode, setNewTierCode] = useState(account?.tier?.code); // May be undefined
const [loading, setLoading] = useState(false);
const [errorText, setErrorText] = useState("");
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
useEffect(() => {
(async () => {
setTiers(await accountApi.billingTiers());
try {
setTiers(await accountApi.billingTiers());
} catch (e) {
setError(e.message);
}
})();
}, []);
@ -96,10 +101,11 @@ const UpgradeDialog = (props) => {
props.onCancel();
} catch (e) {
console.log(`[UpgradeDialog] Error changing billing subscription`, e);
if ((e instanceof UnauthorizedError)) {
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setError(e.message);
}
// FIXME show error
} finally {
setLoading(false);
}
@ -155,7 +161,7 @@ const UpgradeDialog = (props) => {
</Alert>
}
</DialogContent>
<DialogFooter status={errorText}>
<DialogFooter status={error}>
<Button onClick={props.onCancel}>{t("account_upgrade_dialog_button_cancel")}</Button>
<Button onClick={handleSubmit} disabled={!submitAction}>{submitButtonLabel}</Button>
</DialogFooter>

View file

@ -8,7 +8,8 @@ import connectionManager from "../app/ConnectionManager";
import poller from "../app/Poller";
import pruner from "../app/Pruner";
import session from "../app/Session";
import accountApi, {UnauthorizedError} from "../app/AccountApi";
import accountApi from "../app/AccountApi";
import {UnauthorizedError} from "../app/errors";
/**
* Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection
@ -94,7 +95,7 @@ export const useAutoSubscribe = (subscriptions, selected) => {
const eligible = params.topic && !selected && !disallowedTopic(params.topic);
if (eligible) {
const baseUrl = (params.baseUrl) ? expandSecureUrl(params.baseUrl) : config.base_url;
console.log(`[App] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`);
console.log(`[Hooks] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`);
(async () => {
const subscription = await subscriptionManager.add(baseUrl, params.topic);
if (session.exists()) {
@ -105,8 +106,8 @@ export const useAutoSubscribe = (subscriptions, selected) => {
});
await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
} catch (e) {
console.log(`[App] Auto-subscribing failed`, e);
if ((e instanceof UnauthorizedError)) {
console.log(`[Hooks] Auto-subscribing failed`, e);
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
}
}