diff --git a/scm-ui/ui-components/src/ErrorNotification.tsx b/scm-ui/ui-components/src/ErrorNotification.tsx
index 00524efa4e..c43ef73119 100644
--- a/scm-ui/ui-components/src/ErrorNotification.tsx
+++ b/scm-ui/ui-components/src/ErrorNotification.tsx
@@ -21,16 +21,26 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-import React from "react";
-import { WithTranslation, withTranslation } from "react-i18next";
+import React, { FC } from "react";
+import { useTranslation, WithTranslation, withTranslation } from "react-i18next";
import { BackendError, ForbiddenError, UnauthorizedError } from "./errors";
import Notification from "./Notification";
import BackendErrorNotification from "./BackendErrorNotification";
+import { useLocation } from "react-router-dom";
+import { withContextPath } from "./urls";
type Props = WithTranslation & {
error?: Error;
};
+const LoginLink: FC = () => {
+ const [t] = useTranslation("commons");
+ const location = useLocation();
+ const from = encodeURIComponent(location.pathname);
+
+ return {t("errorNotification.loginLink")};
+};
+
class ErrorNotification extends React.Component {
render() {
const { t, error } = this.props;
@@ -40,8 +50,7 @@ class ErrorNotification extends React.Component {
} else if (error instanceof UnauthorizedError) {
return (
- {t("errorNotification.prefix")}: {t("errorNotification.timeout")}{" "}
- {t("errorNotification.loginLink")}
+ {t("errorNotification.prefix")}: {t("errorNotification.timeout")}
);
} else if (error instanceof ForbiddenError) {
diff --git a/scm-ui/ui-components/src/apiclient.ts b/scm-ui/ui-components/src/apiclient.ts
index a371b06571..361038208e 100644
--- a/scm-ui/ui-components/src/apiclient.ts
+++ b/scm-ui/ui-components/src/apiclient.ts
@@ -125,23 +125,15 @@ const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
function handleFailure(response: Response) {
if (!response.ok) {
- if (isBackendError(response)) {
+ if (response.status === 401) {
+ throw new UnauthorizedError("Unauthorized", 401);
+ } else if (response.status === 403) {
+ throw new ForbiddenError("Forbidden", 403);
+ } else if (isBackendError(response)) {
return response.json().then((content: BackendErrorContent) => {
- if (content.errorCode === TOKEN_EXPIRED_ERROR_CODE) {
- window.location.replace(`${contextPath}/login`);
- // Throw error because if redirect is not instantaneous, we want to display something senseful
- throw new UnauthorizedError("Unauthorized", 401);
- } else {
- throw createBackendError(content, response.status);
- }
+ throw createBackendError(content, response.status);
});
} else {
- if (response.status === 401) {
- throw new UnauthorizedError("Unauthorized", 401);
- } else if (response.status === 403) {
- throw new ForbiddenError("Forbidden", 403);
- }
-
throw new Error("server returned status code " + response.status);
}
}
diff --git a/scm-ui/ui-webapp/src/containers/Login.test.ts b/scm-ui/ui-webapp/src/containers/Login.test.ts
new file mode 100644
index 0000000000..5e8d4a2b81
--- /dev/null
+++ b/scm-ui/ui-webapp/src/containers/Login.test.ts
@@ -0,0 +1,62 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { from } from "./Login";
+
+describe("from tests", () => {
+ it("should use default location", () => {
+ const path = from("", {});
+ expect(path).toBe("/");
+ });
+
+ it("should use default location without params", () => {
+ const path = from();
+ expect(path).toBe("/");
+ });
+
+ it("should use default location with null params", () => {
+ const path = from("", null);
+ expect(path).toBe("/");
+ });
+
+ it("should use location from query parameter", () => {
+ const path = from("from=/repos", {});
+ expect(path).toBe("/repos");
+ });
+
+ it("should use location from state", () => {
+ const path = from("", { from: "/users" });
+ expect(path).toBe("/users");
+ });
+
+ it("should prefer location from query parameter", () => {
+ const path = from("from=/groups", { from: "/users" });
+ expect(path).toBe("/groups");
+ });
+
+ it("should decode query param", () => {
+ const path = from(`from=${encodeURIComponent("/admin/plugins/installed")}`);
+ expect(path).toBe("/admin/plugins/installed");
+ });
+});
diff --git a/scm-ui/ui-webapp/src/containers/Login.tsx b/scm-ui/ui-webapp/src/containers/Login.tsx
index f40eef5921..8c25c6a12e 100644
--- a/scm-ui/ui-webapp/src/containers/Login.tsx
+++ b/scm-ui/ui-webapp/src/containers/Login.tsx
@@ -30,6 +30,7 @@ import { getLoginFailure, getMe, isAnonymous, isLoginPending, login } from "../m
import { getLoginInfoLink, getLoginLink } from "../modules/indexResource";
import LoginInfo from "../components/LoginInfo";
import { Me } from "@scm-manager/ui-types";
+import { parse } from "query-string";
type Props = RouteComponentProps & {
authenticated: boolean;
@@ -47,6 +48,18 @@ const HeroSection = styled.section`
padding-top: 2em;
`;
+interface FromObject {
+ from?: string;
+}
+
+/**
+ * @visibleForTesting
+ */
+export const from = (queryString?: string, stateParams?: FromObject | null): string => {
+ const queryParams = parse(queryString || "");
+ return queryParams?.from || stateParams?.from || "/";
+};
+
class Login extends React.Component {
handleLogin = (username: string, password: string): void => {
const { link, login } = this.props;
@@ -54,12 +67,8 @@ class Login extends React.Component {
};
renderRedirect = () => {
- const { from } = this.props.location.state || {
- from: {
- pathname: "/"
- }
- };
- return ;
+ const to = from(window.location.search, this.props.location.state);
+ return ;
};
render() {