Redirect UI if unauthorized API response
This commit is contained in:
parent
1b39ba70cb
commit
3aac1b2715
11 changed files with 148 additions and 77 deletions
|
@ -42,7 +42,6 @@ import (
|
|||
expire tokens
|
||||
auto-refresh tokens from UI
|
||||
reserve topics
|
||||
handle invalid session token
|
||||
purge accounts that were not logged into in X
|
||||
sync subscription display name
|
||||
reset daily limits for users
|
||||
|
|
|
@ -126,7 +126,7 @@ class Api {
|
|||
headers: maybeWithBasicAuth({}, user)
|
||||
});
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return false;
|
||||
throw new UnauthorizedError();
|
||||
} else if (response.status !== 200) {
|
||||
throw new Error(`Unexpected server response ${response.status}`);
|
||||
}
|
||||
|
@ -144,7 +144,9 @@ class Api {
|
|||
method: "DELETE",
|
||||
headers: maybeWithBearerAuth({}, token)
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new UnauthorizedError();
|
||||
} else if (response.status !== 200) {
|
||||
throw new Error(`Unexpected server response ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
@ -175,7 +177,9 @@ class Api {
|
|||
const response = await fetch(url, {
|
||||
headers: maybeWithBearerAuth({}, token)
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
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();
|
||||
|
@ -190,7 +194,9 @@ class Api {
|
|||
method: "DELETE",
|
||||
headers: maybeWithBearerAuth({}, token)
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new UnauthorizedError();
|
||||
} else if (response.status !== 200) {
|
||||
throw new Error(`Unexpected server response ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
@ -205,7 +211,9 @@ class Api {
|
|||
password: password
|
||||
})
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new UnauthorizedError();
|
||||
} else if (response.status !== 200) {
|
||||
throw new Error(`Unexpected server response ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
@ -219,7 +227,9 @@ class Api {
|
|||
headers: maybeWithBearerAuth({}, token),
|
||||
body: body
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new UnauthorizedError();
|
||||
} else if (response.status !== 200) {
|
||||
throw new Error(`Unexpected server response ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
@ -233,7 +243,9 @@ class Api {
|
|||
headers: maybeWithBearerAuth({}, token),
|
||||
body: body
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
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();
|
||||
|
@ -248,7 +260,9 @@ class Api {
|
|||
method: "DELETE",
|
||||
headers: maybeWithBearerAuth({}, token)
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new UnauthorizedError();
|
||||
} else if (response.status !== 200) {
|
||||
throw new Error(`Unexpected server response ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
@ -256,13 +270,21 @@ class Api {
|
|||
|
||||
export class UsernameTakenError extends Error {
|
||||
constructor(username) {
|
||||
super();
|
||||
super("Username taken");
|
||||
this.username = username;
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountCreateLimitReachedError extends Error {
|
||||
// Nothing
|
||||
constructor() {
|
||||
super("Account creation limit reached");
|
||||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends Error {
|
||||
constructor() {
|
||||
super("Unauthorized");
|
||||
}
|
||||
}
|
||||
|
||||
const api = new Api();
|
||||
|
|
|
@ -16,7 +16,7 @@ import DialogTitle from "@mui/material/DialogTitle";
|
|||
import DialogContent from "@mui/material/DialogContent";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import api from "../app/Api";
|
||||
import api, {UnauthorizedError} from "../app/Api";
|
||||
import routes from "./routes";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import {useNavigate, useOutletContext} from "react-router-dom";
|
||||
|
@ -152,6 +152,10 @@ const ChangePassword = () => {
|
|||
console.debug(`[Account] Password changed`);
|
||||
} catch (e) {
|
||||
console.log(`[Account] Error changing password`, e);
|
||||
if ((e instanceof UnauthorizedError)) {
|
||||
session.reset();
|
||||
window.location.href = routes.login;
|
||||
}
|
||||
// TODO show error
|
||||
}
|
||||
};
|
||||
|
@ -238,6 +242,10 @@ const DeleteAccount = () => {
|
|||
window.location.href = routes.app;
|
||||
} catch (e) {
|
||||
console.log(`[Account] Error deleting account`, e);
|
||||
if ((e instanceof UnauthorizedError)) {
|
||||
session.reset();
|
||||
window.location.href = routes.login;
|
||||
}
|
||||
// TODO show error
|
||||
}
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@ import MenuList from '@mui/material/MenuList';
|
|||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||
import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||
import NotificationsOffIcon from '@mui/icons-material/NotificationsOff';
|
||||
import api from "../app/Api";
|
||||
import api, {UnauthorizedError} from "../app/Api";
|
||||
import routes from "./routes";
|
||||
import subscriptionManager from "../app/SubscriptionManager";
|
||||
import logo from "../img/ntfy.svg";
|
||||
|
@ -118,7 +118,15 @@ const SettingsIcons = (props) => {
|
|||
handleClose(event);
|
||||
await subscriptionManager.remove(props.subscription.id);
|
||||
if (session.exists() && props.subscription.remoteId) {
|
||||
await api.deleteAccountSubscription(config.baseUrl, session.token(), props.subscription.remoteId);
|
||||
try {
|
||||
await api.deleteAccountSubscription(config.baseUrl, session.token(), props.subscription.remoteId);
|
||||
} catch (e) {
|
||||
console.log(`[ActionBar] Error unsubscribing`, e);
|
||||
if ((e instanceof UnauthorizedError)) {
|
||||
session.reset();
|
||||
window.location.href = routes.login;
|
||||
}
|
||||
}
|
||||
}
|
||||
const newSelected = await subscriptionManager.first(); // May be undefined
|
||||
if (newSelected) {
|
||||
|
|
|
@ -26,7 +26,7 @@ import {Backdrop, CircularProgress} from "@mui/material";
|
|||
import Home from "./Home";
|
||||
import Login from "./Login";
|
||||
import i18n from "i18next";
|
||||
import api from "../app/Api";
|
||||
import api, {UnauthorizedError} from "../app/Api";
|
||||
import prefs from "../app/Prefs";
|
||||
import session from "../app/Session";
|
||||
import Pricing from "./Pricing";
|
||||
|
@ -96,8 +96,12 @@ const Layout = () => {
|
|||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const acc = await api.getAccount(config.baseUrl, session.token());
|
||||
if (acc) {
|
||||
// TODO this should not live here
|
||||
try {
|
||||
if (!session.token()) {
|
||||
return;
|
||||
}
|
||||
const acc = await api.getAccount(config.baseUrl, session.token());
|
||||
setAccount(acc);
|
||||
if (acc.language) {
|
||||
await i18n.changeLanguage(acc.language);
|
||||
|
@ -116,6 +120,12 @@ const Layout = () => {
|
|||
if (acc.subscriptions) {
|
||||
await subscriptionManager.syncFromRemote(acc.subscriptions);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`[App] Error fetching account`, e);
|
||||
if ((e instanceof UnauthorizedError)) {
|
||||
session.reset();
|
||||
window.location.href = routes.login;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
|
|
@ -4,7 +4,7 @@ import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
|||
import TextField from "@mui/material/TextField";
|
||||
import Button from "@mui/material/Button";
|
||||
import Box from "@mui/material/Box";
|
||||
import api from "../app/Api";
|
||||
import api, {UnauthorizedError} from "../app/Api";
|
||||
import routes from "./routes";
|
||||
import session from "../app/Session";
|
||||
import {NavLink} from "react-router-dom";
|
||||
|
@ -22,17 +22,14 @@ const Login = () => {
|
|||
const user = { username, password };
|
||||
try {
|
||||
const token = await api.login(config.baseUrl, user);
|
||||
if (token) {
|
||||
console.log(`[Login] User auth for user ${user.username} successful, token is ${token}`);
|
||||
session.store(user.username, token);
|
||||
window.location.href = routes.app;
|
||||
} else {
|
||||
console.log(`[Login] User auth for user ${user.username} failed, access denied`);
|
||||
setError(t("Login failed: Invalid username or password"));
|
||||
}
|
||||
console.log(`[Login] User auth for user ${user.username} successful, token is ${token}`);
|
||||
session.store(user.username, token);
|
||||
window.location.href = routes.app;
|
||||
} catch (e) {
|
||||
console.log(`[Login] User auth for user ${user.username} failed`, e);
|
||||
if (e && e.message) {
|
||||
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."))
|
||||
|
|
|
@ -34,8 +34,9 @@ import DialogActions from "@mui/material/DialogActions";
|
|||
import userManager from "../app/UserManager";
|
||||
import {playSound, shuffle, sounds, validTopic, validUrl} from "../app/utils";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import api from "../app/Api";
|
||||
import api, {UnauthorizedError} from "../app/Api";
|
||||
import session from "../app/Session";
|
||||
import routes from "./routes";
|
||||
|
||||
const Preferences = () => {
|
||||
return (
|
||||
|
@ -72,13 +73,11 @@ const Sound = () => {
|
|||
const sound = useLiveQuery(async () => prefs.sound());
|
||||
const handleChange = async (ev) => {
|
||||
await prefs.setSound(ev.target.value);
|
||||
if (session.exists()) {
|
||||
await api.updateAccountSettings(config.baseUrl, session.token(), {
|
||||
notification: {
|
||||
sound: ev.target.value
|
||||
}
|
||||
});
|
||||
}
|
||||
await maybeUpdateAccountSettings({
|
||||
notification: {
|
||||
sound: ev.target.value
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!sound) {
|
||||
return null; // While loading
|
||||
|
@ -112,13 +111,11 @@ const MinPriority = () => {
|
|||
const minPriority = useLiveQuery(async () => prefs.minPriority());
|
||||
const handleChange = async (ev) => {
|
||||
await prefs.setMinPriority(ev.target.value);
|
||||
if (session.exists()) {
|
||||
await api.updateAccountSettings(config.baseUrl, session.token(), {
|
||||
notification: {
|
||||
min_priority: ev.target.value
|
||||
}
|
||||
});
|
||||
}
|
||||
await maybeUpdateAccountSettings({
|
||||
notification: {
|
||||
min_priority: ev.target.value
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!minPriority) {
|
||||
return null; // While loading
|
||||
|
@ -162,13 +159,11 @@ const DeleteAfter = () => {
|
|||
const deleteAfter = useLiveQuery(async () => prefs.deleteAfter());
|
||||
const handleChange = async (ev) => {
|
||||
await prefs.setDeleteAfter(ev.target.value);
|
||||
if (session.exists()) {
|
||||
await api.updateAccountSettings(config.baseUrl, session.token(), {
|
||||
notification: {
|
||||
delete_after: ev.target.value
|
||||
}
|
||||
});
|
||||
}
|
||||
await maybeUpdateAccountSettings({
|
||||
notification: {
|
||||
delete_after: ev.target.value
|
||||
}
|
||||
});
|
||||
}
|
||||
if (deleteAfter === null || deleteAfter === undefined) { // !deleteAfter will not work with "0"
|
||||
return null; // While loading
|
||||
|
@ -466,11 +461,9 @@ const Language = () => {
|
|||
|
||||
const handleChange = async (ev) => {
|
||||
await i18n.changeLanguage(ev.target.value);
|
||||
if (session.exists()) {
|
||||
await api.updateAccountSettings(config.baseUrl, session.token(), {
|
||||
language: ev.target.value
|
||||
});
|
||||
}
|
||||
await maybeUpdateAccountSettings({
|
||||
language: ev.target.value
|
||||
});
|
||||
};
|
||||
|
||||
// Remember: Flags are not languages. Don't put flags next to the language in the list.
|
||||
|
@ -670,4 +663,19 @@ const AccessControlDialog = (props) => {
|
|||
};
|
||||
*/
|
||||
|
||||
const maybeUpdateAccountSettings = async (payload) => {
|
||||
if (!session.exists()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.updateAccountSettings(config.baseUrl, session.token(), payload);
|
||||
} catch (e) {
|
||||
console.log(`[Preferences] Error updating account settings`, e);
|
||||
if ((e instanceof UnauthorizedError)) {
|
||||
session.reset();
|
||||
window.location.href = routes.login;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default Preferences;
|
||||
|
|
|
@ -22,11 +22,12 @@ import {basicAuth, formatBytes, maybeWithBasicAuth, topicShortUrl, topicUrl, val
|
|||
import Box from "@mui/material/Box";
|
||||
import AttachmentIcon from "./AttachmentIcon";
|
||||
import DialogFooter from "./DialogFooter";
|
||||
import api from "../app/Api";
|
||||
import api, {UnauthorizedError} from "../app/Api";
|
||||
import userManager from "../app/UserManager";
|
||||
import EmojiPicker from "./EmojiPicker";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
import session from "../app/Session";
|
||||
import routes from "./routes";
|
||||
|
||||
const PublishDialog = (props) => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -178,7 +179,12 @@ const PublishDialog = (props) => {
|
|||
setAttachFileError("");
|
||||
} catch (e) {
|
||||
console.log(`[PublishDialog] Retrieving attachment limits failed`, e);
|
||||
setAttachFileError(""); // Reset error (rely on server-side checking)
|
||||
if ((e instanceof UnauthorizedError)) {
|
||||
session.reset();
|
||||
window.location.href = routes.login;
|
||||
} else {
|
||||
setAttachFileError(""); // Reset error (rely on server-side checking)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react';
|
|||
import TextField from "@mui/material/TextField";
|
||||
import Button from "@mui/material/Button";
|
||||
import Box from "@mui/material/Box";
|
||||
import api, {AccountCreateLimitReachedError, UsernameTakenError} from "../app/Api";
|
||||
import api, {AccountCreateLimitReachedError, UnauthorizedError, UsernameTakenError} from "../app/Api";
|
||||
import routes from "./routes";
|
||||
import session from "../app/Session";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
@ -24,14 +24,9 @@ const Signup = () => {
|
|||
try {
|
||||
await api.createAccount(config.baseUrl, user.username, user.password);
|
||||
const token = await api.login(config.baseUrl, user);
|
||||
if (token) {
|
||||
console.log(`[Signup] User signup for user ${user.username} successful, token is ${token}`);
|
||||
session.store(user.username, token);
|
||||
window.location.href = routes.app;
|
||||
} else {
|
||||
console.log(`[Signup] Signup for user ${user.username} failed, access denied`);
|
||||
setError(t("Login failed: Invalid username or password"));
|
||||
}
|
||||
console.log(`[Signup] User signup for user ${user.username} successful, token is ${token}`);
|
||||
session.store(user.username, token);
|
||||
window.location.href = routes.app;
|
||||
} catch (e) {
|
||||
console.log(`[Signup] Signup for user ${user.username} failed`, e);
|
||||
if ((e instanceof UsernameTakenError)) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import DialogContentText from '@mui/material/DialogContentText';
|
|||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
|
||||
import theme from "./theme";
|
||||
import api from "../app/Api";
|
||||
import api, {UnauthorizedError} from "../app/Api";
|
||||
import {randomAlphanumericString, topicUrl, validTopic, validUrl} from "../app/utils";
|
||||
import userManager from "../app/UserManager";
|
||||
import subscriptionManager from "../app/SubscriptionManager";
|
||||
|
@ -16,6 +16,7 @@ import poller from "../app/Poller";
|
|||
import DialogFooter from "./DialogFooter";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import session from "../app/Session";
|
||||
import routes from "./routes";
|
||||
|
||||
const publicBaseUrl = "https://ntfy.sh";
|
||||
|
||||
|
@ -25,14 +26,23 @@ const SubscribeDialog = (props) => {
|
|||
const [showLoginPage, setShowLoginPage] = useState(false);
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const handleSuccess = async () => {
|
||||
console.log(`[SubscribeDialog] Subscribing to topic ${topic}`);
|
||||
const actualBaseUrl = (baseUrl) ? baseUrl : config.baseUrl;
|
||||
const subscription = await subscriptionManager.add(actualBaseUrl, topic);
|
||||
if (session.exists()) {
|
||||
const remoteSubscription = await api.addAccountSubscription(config.baseUrl, session.token(), {
|
||||
base_url: actualBaseUrl,
|
||||
topic: topic
|
||||
});
|
||||
await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
|
||||
try {
|
||||
const remoteSubscription = await api.addAccountSubscription(config.baseUrl, session.token(), {
|
||||
base_url: actualBaseUrl,
|
||||
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.reset();
|
||||
window.location.href = routes.login;
|
||||
}
|
||||
}
|
||||
}
|
||||
poller.pollInBackground(subscription); // Dangle!
|
||||
props.onSuccess(subscription);
|
||||
|
|
|
@ -8,7 +8,7 @@ import connectionManager from "../app/ConnectionManager";
|
|||
import poller from "../app/Poller";
|
||||
import pruner from "../app/Pruner";
|
||||
import session from "../app/Session";
|
||||
import api from "../app/Api";
|
||||
import api, {UnauthorizedError} from "../app/Api";
|
||||
|
||||
/**
|
||||
* Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection
|
||||
|
@ -64,11 +64,19 @@ export const useAutoSubscribe = (subscriptions, selected) => {
|
|||
(async () => {
|
||||
const subscription = await subscriptionManager.add(baseUrl, params.topic);
|
||||
if (session.exists()) {
|
||||
const remoteSubscription = await api.addAccountSubscription(config.baseUrl, session.token(), {
|
||||
base_url: baseUrl,
|
||||
topic: params.topic
|
||||
});
|
||||
await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
|
||||
try {
|
||||
const remoteSubscription = await api.addAccountSubscription(config.baseUrl, session.token(), {
|
||||
base_url: baseUrl,
|
||||
topic: params.topic
|
||||
});
|
||||
await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
|
||||
} catch (e) {
|
||||
console.log(`[App] Auto-subscribing failed`, e);
|
||||
if ((e instanceof UnauthorizedError)) {
|
||||
session.reset();
|
||||
window.location.href = routes.login;
|
||||
}
|
||||
}
|
||||
}
|
||||
poller.pollInBackground(subscription); // Dangle!
|
||||
})();
|
||||
|
|
Loading…
Reference in a new issue