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

@ -18,6 +18,7 @@ import subscriptionManager from "./SubscriptionManager";
import i18n from "i18next";
import prefs from "./Prefs";
import routes from "../components/routes";
import {fetchOrThrow, throwAppError, UnauthorizedError} from "./errors";
const delayMillis = 45000; // 45 seconds
const intervalMillis = 900000; // 15 minutes
@ -39,16 +40,11 @@ class AccountApi {
async login(user) {
const url = accountTokenUrl(config.base_url);
console.log(`[AccountApi] Checking auth for ${url}`);
const response = await fetch(url, {
const response = await fetchOrThrow(url, {
method: "POST",
headers: withBasicAuth({}, user.username, user.password)
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
const json = await response.json();
const json = await response.json(); // May throw SyntaxError
if (!json.token) {
throw new Error(`Unexpected server response: Cannot find token`);
}
@ -58,15 +54,10 @@ class AccountApi {
async logout() {
const url = accountTokenUrl(config.base_url);
console.log(`[AccountApi] Logging out from ${url} using token ${session.token()}`);
const response = await fetch(url, {
await fetchOrThrow(url, {
method: "DELETE",
headers: withBearerAuth({}, session.token())
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async create(username, password) {
@ -76,31 +67,19 @@ class AccountApi {
password: password
});
console.log(`[AccountApi] Creating user account ${url}`);
const response = await fetch(url, {
await fetchOrThrow(url, {
method: "POST",
body: body
});
if (response.status === 409) {
throw new UsernameTakenError(username);
} else if (response.status === 429) {
throw new AccountCreateLimitReachedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async get() {
const url = accountUrl(config.base_url);
console.log(`[AccountApi] Fetching user account ${url}`);
const response = await fetch(url, {
const response = await fetchOrThrow(url, {
headers: withBearerAuth({}, session.token())
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
const account = await response.json();
const account = await response.json(); // May throw SyntaxError
console.log(`[AccountApi] Account`, account);
if (this.listener) {
this.listener(account);
@ -111,26 +90,19 @@ class AccountApi {
async delete(password) {
const url = accountUrl(config.base_url);
console.log(`[AccountApi] Deleting user account ${url}`);
const response = await fetch(url, {
await fetchOrThrow(url, {
method: "DELETE",
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
password: password
})
});
if (response.status === 400) {
throw new IncorrectPasswordError();
} else if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async changePassword(currentPassword, newPassword) {
const url = accountPasswordUrl(config.base_url);
console.log(`[AccountApi] Changing account password ${url}`);
const response = await fetch(url, {
await fetchOrThrow(url, {
method: "POST",
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
@ -138,13 +110,6 @@ class AccountApi {
new_password: newPassword
})
});
if (response.status === 400) {
throw new IncorrectPasswordError();
} else if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async createToken(label, expires) {
@ -154,16 +119,11 @@ class AccountApi {
expires: (expires > 0) ? Math.floor(Date.now() / 1000) + expires : 0
};
console.log(`[AccountApi] Creating user access token ${url}`);
const response = await fetch(url, {
await fetchOrThrow(url, {
method: "POST",
headers: withBearerAuth({}, session.token()),
body: JSON.stringify(body)
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async updateToken(token, label, expires) {
@ -176,22 +136,17 @@ class AccountApi {
body.expires = Math.floor(Date.now() / 1000) + expires;
}
console.log(`[AccountApi] Creating user access token ${url}`);
const response = await fetch(url, {
await fetchOrThrow(url, {
method: "PATCH",
headers: withBearerAuth({}, session.token()),
body: JSON.stringify(body)
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async extendToken() {
const url = accountTokenUrl(config.base_url);
console.log(`[AccountApi] Extending user access token ${url}`);
const response = await fetch(url, {
await fetchOrThrow(url, {
method: "PATCH",
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
@ -199,58 +154,38 @@ class AccountApi {
expires: Math.floor(Date.now() / 1000) + 6220800 // FIXME
})
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async deleteToken(token) {
const url = accountTokenUrl(config.base_url);
console.log(`[AccountApi] Deleting user access token ${url}`);
const response = await fetch(url, {
await fetchOrThrow(url, {
method: "DELETE",
headers: withBearerAuth({"X-Token": token}, session.token())
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async updateSettings(payload) {
const url = accountSettingsUrl(config.base_url);
const body = JSON.stringify(payload);
console.log(`[AccountApi] Updating user account ${url}: ${body}`);
const response = await fetch(url, {
await fetchOrThrow(url, {
method: "PATCH",
headers: withBearerAuth({}, session.token()),
body: body
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async addSubscription(payload) {
const url = accountSubscriptionUrl(config.base_url);
const body = JSON.stringify(payload);
console.log(`[AccountApi] Adding user subscription ${url}: ${body}`);
const response = await fetch(url, {
const response = await fetchOrThrow(url, {
method: "POST",
headers: withBearerAuth({}, session.token()),
body: body
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
const subscription = await response.json();
const subscription = await response.json(); // May throw SyntaxError
console.log(`[AccountApi] Subscription`, subscription);
return subscription;
}
@ -259,17 +194,12 @@ class AccountApi {
const url = accountSubscriptionSingleUrl(config.base_url, remoteId);
const body = JSON.stringify(payload);
console.log(`[AccountApi] Updating user subscription ${url}: ${body}`);
const response = await fetch(url, {
const response = await fetchOrThrow(url, {
method: "PATCH",
headers: withBearerAuth({}, session.token()),
body: body
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
const subscription = await response.json();
const subscription = await response.json(); // May throw SyntaxError
console.log(`[AccountApi] Subscription`, subscription);
return subscription;
}
@ -277,21 +207,16 @@ class AccountApi {
async deleteSubscription(remoteId) {
const url = accountSubscriptionSingleUrl(config.base_url, remoteId);
console.log(`[AccountApi] Removing user subscription ${url}`);
const response = await fetch(url, {
await fetchOrThrow(url, {
method: "DELETE",
headers: withBearerAuth({}, session.token())
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async upsertReservation(topic, everyone) {
const url = accountReservationUrl(config.base_url);
console.log(`[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}`);
const response = await fetch(url, {
await fetchOrThrow(url, {
method: "POST",
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
@ -299,13 +224,6 @@ class AccountApi {
everyone: everyone
})
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status === 409) {
throw new TopicReservedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async deleteReservation(topic, deleteMessages) {
@ -314,25 +232,17 @@ class AccountApi {
const headers = {
"X-Delete-Messages": deleteMessages ? "true" : "false"
}
const response = await fetch(url, {
await fetchOrThrow(url, {
method: "DELETE",
headers: withBearerAuth(headers, session.token())
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async billingTiers() {
const url = tiersUrl(config.base_url);
console.log(`[AccountApi] Fetching billing tiers`);
const response = await fetch(url); // No auth needed!
if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
return await response.json();
const response = await fetchOrThrow(url); // No auth needed!
return await response.json(); // May throw SyntaxError
}
async createBillingSubscription(tier) {
@ -347,48 +257,33 @@ class AccountApi {
async upsertBillingSubscription(method, tier) {
const url = accountBillingSubscriptionUrl(config.base_url);
const response = await fetch(url, {
const response = await fetchOrThrow(url, {
method: method,
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
tier: tier
})
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
return await response.json();
return await response.json(); // May throw SyntaxError
}
async deleteBillingSubscription() {
const url = accountBillingSubscriptionUrl(config.base_url);
console.log(`[AccountApi] Cancelling billing subscription`);
const response = await fetch(url, {
await fetchOrThrow(url, {
method: "DELETE",
headers: withBearerAuth({}, session.token())
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
async createBillingPortalSession() {
const url = accountBillingPortalUrl(config.base_url);
console.log(`[AccountApi] Creating billing portal session`);
const response = await fetch(url, {
const response = await fetchOrThrow(url, {
method: "POST",
headers: withBearerAuth({}, session.token())
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
return await response.json();
return await response.json(); // May throw SyntaxError
}
async sync() {
@ -418,7 +313,7 @@ class AccountApi {
return account;
} catch (e) {
console.log(`[AccountApi] Error fetching account`, e);
if ((e instanceof UnauthorizedError)) {
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
}
}
@ -472,37 +367,5 @@ export const Permission = {
DENY_ALL: "deny-all"
};
export class UsernameTakenError extends Error {
constructor(username) {
super("Username taken");
this.username = username;
}
}
export class TopicReservedError extends Error {
constructor(topic) {
super("Topic already reserved");
this.topic = topic;
}
}
export class AccountCreateLimitReachedError extends Error {
constructor() {
super("Account creation limit reached");
}
}
export class IncorrectPasswordError extends Error {
constructor() {
super("Password incorrect");
}
}
export class UnauthorizedError extends Error {
constructor() {
super("Unauthorized");
}
}
const accountApi = new AccountApi();
export default accountApi;

View file

@ -8,6 +8,7 @@ import {
topicUrlJsonPollWithSince
} from "./utils";
import userManager from "./UserManager";
import {fetchOrThrow} from "./errors";
class Api {
async poll(baseUrl, topic, since) {
@ -35,15 +36,11 @@ class Api {
message: message,
...options
};
const response = await fetch(baseUrl, {
await fetchOrThrow(baseUrl, {
method: 'PUT',
body: JSON.stringify(body),
headers: maybeWithAuth(headers, user)
});
if (response.status < 200 || response.status > 299) {
throw new Error(`Unexpected response: ${response.status}`);
}
return response;
}
/**
@ -108,8 +105,6 @@ class Api {
});
if (response.status >= 200 && response.status <= 299) {
return true;
} else if (!user && response.status === 404) {
return true; // Special case: Anonymous login to old servers return 404 since /<topic>/auth doesn't exist
} else if (response.status === 401 || response.status === 403) { // See server/server.go
return false;
}

66
web/src/app/errors.js Normal file
View file

@ -0,0 +1,66 @@
// This is a subset of, and the counterpart to errors.go
export const fetchOrThrow = async (url, options) => {
const response = await fetch(url, options);
if (response.status !== 200) {
await throwAppError(response);
}
return response; // Promise!
};
export const throwAppError = async (response) => {
if (response.status === 401 || response.status === 403) {
console.log(`[Error] HTTP ${response.status}`, response);
throw new UnauthorizedError();
}
const error = await maybeToJson(response);
if (error?.code) {
console.log(`[Error] HTTP ${response.status}, ntfy error ${error.code}: ${error.error || ""}`, response);
if (error.code === UserExistsError.CODE) {
throw new UserExistsError();
} else if (error.code === TopicReservedError.CODE) {
throw new TopicReservedError();
} else if (error.code === AccountCreateLimitReachedError.CODE) {
throw new AccountCreateLimitReachedError();
} else if (error.code === IncorrectPasswordError.CODE) {
throw new IncorrectPasswordError();
} else if (error?.error) {
throw new Error(`Error ${error.code}: ${error.error}`);
}
}
console.log(`[Error] HTTP ${response.status}, not a ntfy error`, response);
throw new Error(`Unexpected response ${response.status}`);
};
const maybeToJson = async (response) => {
try {
return await response.json();
} catch (e) {
return null;
}
}
export class UnauthorizedError extends Error {
constructor() { super("Unauthorized"); }
}
export class UserExistsError extends Error {
static CODE = 40901; // errHTTPConflictUserExists
constructor() { super("Username already exists"); }
}
export class TopicReservedError extends Error {
static CODE = 40902; // errHTTPConflictTopicReserved
constructor() { super("Topic already reserved"); }
}
export class AccountCreateLimitReachedError extends Error {
static CODE = 42906; // errHTTPTooManyRequestsLimitAccountCreation
constructor() { super("Account creation limit reached"); }
}
export class IncorrectPasswordError extends Error {
static CODE = 40026; // errHTTPBadRequestIncorrectPasswordConfirmation
constructor() { super("Password incorrect"); }
}

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);
}
}