diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 669cfee8e3..82014d7fc6 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,11 @@ +------------------------------------------------------------------- +Thu Jun 6 07:43:50 UTC 2024 - Knut Anderssen + +- Try to reconnect silently when the WebSocket is closed displaying + a page error if it is not possible (gh#openSUSE/agama#1254). +- Display a different login error message depending on the request + response (gh@openSUSE/agama#1274). + ------------------------------------------------------------------- Thu May 23 07:28:44 UTC 2024 - Josef Reidinger diff --git a/web/src/components/core/LoginPage.jsx b/web/src/components/core/LoginPage.jsx index c04c8301e9..c0629dd15f 100644 --- a/web/src/components/core/LoginPage.jsx +++ b/web/src/components/core/LoginPage.jsx @@ -26,7 +26,7 @@ import { Navigate } from "react-router-dom"; import { ActionGroup, Button, Form, FormGroup } from "@patternfly/react-core"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; -import { useAuth } from "~/context/auth"; +import { AuthErrors, useAuth } from "~/context/auth"; import { About, FormValidationError, If, Page, PasswordInput, Section } from "~/components/core"; import { Center } from "~/components/layout"; @@ -39,14 +39,21 @@ import { Center } from "~/components/layout"; export default function LoginPage() { const [password, setPassword] = useState(""); const [error, setError] = useState(false); - const { isLoggedIn, login: loginFn } = useAuth(); + const { isLoggedIn, login: loginFn, error: loginError } = useAuth(); const login = async (e) => { e.preventDefault(); const result = await loginFn(password); - setError(!result); + + setError(result.status !== 200); }; + const errorMessage = (authError) => { + if (authError === AuthErrors.AUTH) + return _("Could not log in. Please, make sure that the password is correct."); + + return _("Could not authenticate against the server, please check it."); + }; if (isLoggedIn) { return ; } @@ -83,7 +90,7 @@ export default function LoginPage() { condition={error} then={ } /> diff --git a/web/src/components/core/LoginPage.test.jsx b/web/src/components/core/LoginPage.test.jsx index d0192caf13..f927657c01 100644 --- a/web/src/components/core/LoginPage.test.jsx +++ b/web/src/components/core/LoginPage.test.jsx @@ -23,16 +23,19 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { LoginPage } from "~/components/core"; +import { AuthErrors } from "~/context/auth"; let mockIsAuthenticated; const mockLoginFn = jest.fn(); +let mockLoginError; jest.mock("~/context/auth", () => ({ ...jest.requireActual("~/context/auth"), useAuth: () => { return { isAuthenticated: mockIsAuthenticated, - login: mockLoginFn + login: mockLoginFn, + error: mockLoginError }; } })); @@ -40,6 +43,8 @@ jest.mock("~/context/auth", () => ({ describe("LoginPage", () => { beforeAll(() => { mockIsAuthenticated = false; + mockLoginError = null; + mockLoginFn.mockResolvedValue({ status: 200 }); jest.spyOn(console, "error").mockImplementation(); }); @@ -65,6 +70,48 @@ describe("LoginPage", () => { expect(mockLoginFn).toHaveBeenCalledWith("s3cr3t"); }); + describe("and the entered password is wrong", () => { + beforeAll(() => { + mockLoginFn.mockResolvedValue({ status: 400 }); + mockLoginError = AuthErrors.AUTH; + }); + + it("renders an authentication error", async () => { + const { user } = plainRender(); + const form = screen.getByRole("form", { name: "Login form" }); + const passwordInput = within(form).getByLabelText("Password input"); + const loginButton = within(form).getByRole("button", { name: "Log in" }); + + await user.type(passwordInput, "s3cr3t"); + await user.click(loginButton); + + expect(mockLoginFn).toHaveBeenCalledWith("s3cr3t"); + const form_error = screen.getByRole("form", { name: "Login form" }); + within(form_error).getByText(/Could not log in/); + }); + }); + + describe("and the server is down", () => { + beforeAll(() => { + mockLoginFn.mockResolvedValue({ status: 504 }); + mockLoginError = AuthErrors.SERVER; + }); + + it("renders a server error text", async () => { + const { user } = plainRender(); + const form = screen.getByRole("form", { name: "Login form" }); + const passwordInput = within(form).getByLabelText("Password input"); + const loginButton = within(form).getByRole("button", { name: "Log in" }); + + await user.type(passwordInput, "s3cr3t"); + await user.click(loginButton); + + expect(mockLoginFn).toHaveBeenCalledWith("s3cr3t"); + const form_error = screen.getByRole("form", { name: "Login form" }); + within(form_error).getByText(/Could not authenticate/); + }); + }); + it("renders a button to know more about the project", async () => { const { user } = plainRender(); const button = screen.getByRole("button", { name: "What is this?" }); diff --git a/web/src/context/auth.jsx b/web/src/context/auth.jsx index cc4f5389a7..74c528c721 100644 --- a/web/src/context/auth.jsx +++ b/web/src/context/auth.jsx @@ -37,12 +37,19 @@ function useAuth() { return context; } +const AuthErrors = Object.freeze({ + SERVER: "server", + AUTH: "auth", + OTHER: "other" +}); + /** * @param {object} props * @param {React.ReactNode} [props.children] - content to display within the provider */ function AuthProvider({ children }) { const [isLoggedIn, setIsLoggedIn] = useState(false); + const [error, setError] = useState(null); const login = useCallback(async (password) => { const response = await fetch("/api/auth", { @@ -50,9 +57,17 @@ function AuthProvider({ children }) { body: JSON.stringify({ password }), headers: { "Content-Type": "application/json" }, }); + const result = response.status === 200; + if ((response.status >= 500) && (response.status < 600)) { + setError(AuthErrors.SERVER); + } + if ((response.status >= 400) && (response.status < 500)) { + setError(AuthErrors.AUTH); + } setIsLoggedIn(result); - return result; + + return response; }, []); const logout = useCallback(async () => { @@ -68,15 +83,21 @@ function AuthProvider({ children }) { }) .then((response) => { setIsLoggedIn(response.status === 200); + if ((response.status >= 500) && (response.status < 600)) { + setError(AuthErrors.SERVER); + } + if ((response.status >= 400) && (response.status < 500)) { + setError(AuthErrors.AUTH); + } }) .catch(() => setIsLoggedIn(false)); }, []); return ( - + {children} ); } -export { AuthProvider, useAuth }; +export { AuthProvider, useAuth, AuthErrors };