JS error handling
This commit is contained in:
parent
180a7df1e7
commit
0885951a67
20 changed files with 369 additions and 366 deletions
|
@ -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;
|
||||
|
|
|
@ -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
66
web/src/app/errors.js
Normal 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"); }
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue