Repository: CERT-Polska/mwdb-core (language: python)

Issue:
Better registration form
**Feature Category**

- [ ] Correctness
- [x] User Interface / User Experience
- [ ] Performance
- [ ] Other (please explain)

**Describe the problem**

<!-- A clear and concise description of the problem. -->
Current registration component is a little bit deprecated compared to the nwe login view.

**Describe the solution you'd like**

<!-- If you have a solution in mind, please describe it. -->
Design and implementation of a new, more user-friendly view.
Maybe we could resign or make optional some of fields.



Discussion / hints:
The same issue is in recover password form, so i make change layout of this component also.

Fix patch:
diff --git a/mwdb/web/src/commons/hooks/index.js b/mwdb/web/src/commons/hooks/index.js
index d42ff7f06..734429390 100644
--- a/mwdb/web/src/commons/hooks/index.js
+++ b/mwdb/web/src/commons/hooks/index.js
@@ -1,1 +1,2 @@
-export * from "./useCheckCapabilities";
+export { useNavRedirect } from "./useNavRedirect";
+export { useCheckCapabilities } from "./useCheckCapabilities";
diff --git a/mwdb/web/src/commons/hooks/useNavRedirect.js b/mwdb/web/src/commons/hooks/useNavRedirect.js
new file mode 100644
index 000000000..85df02b24
--- /dev/null
+++ b/mwdb/web/src/commons/hooks/useNavRedirect.js
@@ -0,0 +1,22 @@
+import { useCallback } from "react";
+import { useNavigate, useLocation } from "react-router-dom";
+
+export function useNavRedirect() {
+    const navigate = useNavigate();
+    const location = useLocation();
+
+    function redirectTo(url) {
+        navigate(url);
+    }
+
+    const goBackToPrevLocation = useCallback(() => {
+        const locationState = location.state || {};
+        const prevLocation = locationState.prevLocation || "/";
+        navigate(prevLocation);
+    }, [location]);
+
+    return {
+        goBackToPrevLocation,
+        redirectTo,
+    };
+}
diff --git a/mwdb/web/src/commons/ui/FormError.jsx b/mwdb/web/src/commons/ui/FormError.jsx
new file mode 100644
index 000000000..ad9043569
--- /dev/null
+++ b/mwdb/web/src/commons/ui/FormError.jsx
@@ -0,0 +1,15 @@
+import React from "react";
+import { isNil } from "lodash";
+
+export default function FormError(props) {
+    const { errorField } = props;
+    if (isNil(errorField)) {
+        return <></>;
+    }
+
+    return (
+        <div className="invalid-feedback" style={{ display: "block" }}>
+            {errorField.message}
+        </div>
+    );
+}
diff --git a/mwdb/web/src/commons/ui/Label.jsx b/mwdb/web/src/commons/ui/Label.jsx
new file mode 100644
index 000000000..89cc0b756
--- /dev/null
+++ b/mwdb/web/src/commons/ui/Label.jsx
@@ -0,0 +1,11 @@
+import React from "react";
+
+export default function Label(props) {
+    const { label, required, htmlFor } = props;
+
+    return (
+        <label className={required ? "required" : ""} htmlFor={htmlFor}>
+            {label}
+        </label>
+    );
+}
diff --git a/mwdb/web/src/commons/ui/LoadingSpinner.jsx b/mwdb/web/src/commons/ui/LoadingSpinner.jsx
new file mode 100644
index 000000000..697ac9d9d
--- /dev/null
+++ b/mwdb/web/src/commons/ui/LoadingSpinner.jsx
@@ -0,0 +1,21 @@
+import React, { useMemo } from "react";
+
+export default function LoadingSpinner(props) {
+    //types is one of: [primary, secondary, success, danger, warning, info, light, dark]
+    const { loading, type } = props;
+    if (!loading) {
+        return <></>;
+    }
+
+    const className = useMemo(() => {
+        return `spinner-border ${
+            type ? `text-${type}` : ""
+        } mr-2 spinner-border-sm`.trim();
+    }, [type]);
+
+    return (
+        <span className={className} role="status">
+            <span className="sr-only">Loading...</span>
+        </span>
+    );
+}
diff --git a/mwdb/web/src/commons/ui/index.jsx b/mwdb/web/src/commons/ui/index.jsx
index f457e3dc9..794cec76c 100644
--- a/mwdb/web/src/commons/ui/index.jsx
+++ b/mwdb/web/src/commons/ui/index.jsx
@@ -20,6 +20,9 @@ export { default as SortedList } from "./SortedList";
 export { default as View, useViewAlert } from "./View";
 export { default as ActionCopyToClipboard } from "./ActionCopyToClipboard";
 export { RequiresAuth, RequiresCapability } from "./RequiresAuth";
+export { default as FormError } from "./FormError";
+export { default as Label } from "./Label";
+export { default as LoadingSpinner } from "./LoadingSpinner";
 
 export { Tag, TagList, getStyleForTag } from "./Tag";
 export {
diff --git a/mwdb/web/src/components/UserLogin.jsx b/mwdb/web/src/components/UserLogin.jsx
index 79e14d578..2c2868cb2 100644
--- a/mwdb/web/src/components/UserLogin.jsx
+++ b/mwdb/web/src/components/UserLogin.jsx
@@ -104,7 +104,7 @@ export default function UserLogin() {
                 >
                     <Extension ident="userLoginNote" />
                     <div className="form-group">
-                        <label>Login</label>
+                        <label className="required">Login</label>
                         <input
                             type="text"
                             name="login"
@@ -115,7 +115,7 @@ export default function UserLogin() {
                         />
                     </div>
                     <div className="form-group">
-                        <label>Password</label>
+                        <label className="required">Password</label>
                         <input
                             type="password"
                             name="password"
diff --git a/mwdb/web/src/components/UserPasswordRecover.jsx b/mwdb/web/src/components/UserPasswordRecover.jsx
index a162f18ef..ed82c973e 100644
--- a/mwdb/web/src/components/UserPasswordRecover.jsx
+++ b/mwdb/web/src/components/UserPasswordRecover.jsx
@@ -1,109 +1,167 @@
-import React, { useState, useContext } from "react";
+import React, { useState, useContext, useRef } from "react";
 import ReCAPTCHA from "react-google-recaptcha";
 import { toast } from "react-toastify";
+import { useForm } from "react-hook-form";
+import { yupResolver } from "@hookform/resolvers/yup";
+import * as Yup from "yup";
 
 import { api } from "@mwdb-web/commons/api";
 import { ConfigContext } from "@mwdb-web/commons/config";
-import { View, getErrorMessage } from "@mwdb-web/commons/ui";
+import {
+    View,
+    getErrorMessage,
+    Label,
+    FormError,
+    LoadingSpinner,
+} from "@mwdb-web/commons/ui";
+import { useNavRedirect } from "@mwdb-web/commons/hooks";
 
-export default function UserPasswordRecover() {
-    const initialState = {
-        login: "",
-        email: "",
-    };
+const formFields = {
+    login: "login",
+    email: "email",
+    recaptcha: "recaptcha",
+};
 
-    const config = useContext(ConfigContext);
-    const [fieldState, setFieldState] = useState(initialState);
-    const [success, setSuccess] = useState(false);
-    const [recaptcha, setRecaptcha] = useState(null);
+const validationSchema = Yup.object().shape({
+    [formFields.login]: Yup.string().required("Login is required"),
+    [formFields.email]: Yup.string()
+        .required("Email is required")
+        .matches(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g, "Value is not email"),
+    [formFields.recaptcha]: Yup.string()
+        .nullable()
+        .required("Recaptcha is required"),
+});
 
-    const handleInputChange = (event) => {
-        const value = event.target.value;
-        const name = event.target.name;
+const formOptions = {
+    resolver: yupResolver(validationSchema),
+    mode: "onSubmit",
+    reValidateMode: "onSubmit",
+    shouldFocusError: true,
+};
+
+export default function UserPasswordRecover() {
+    const config = useContext(ConfigContext);
+    const { redirectTo } = useNavRedirect();
+    const {
+        register,
+        handleSubmit,
+        formState: { errors },
+        reset,
+        setValue,
+    } = useForm(formOptions);
 
-        setFieldState((fieldState) => ({
-            ...fieldState,
-            [name]: value,
-        }));
-    };
+    const [loading, setLoading] = useState(false);
+    const captchaRef = useRef(null);
 
-    async function recoverPassword() {
+    async function recoverPassword(values) {
         try {
+            setLoading(true);
             await api.authRecoverPassword(
-                fieldState.login,
-                fieldState.email,
-                recaptcha
+                values.login,
+                values.email,
+                values.recaptcha
             );
-            setSuccess(true);
+
             toast("Password reset link has been sent to the e-mail address", {
                 type: "success",
             });
+            setLoading(false);
+            redirectTo("/login");
         } catch (error) {
             toast(getErrorMessage(error), {
                 type: "error",
             });
-            setFieldState(initialState);
+            setLoading(false);
+            reset();
+        } finally {
+            captchaRef.current?.reset();
+            setValue(formFields.recaptcha, null);
         }
     }
 
-    const onCaptchaChange = (value) => {
-        setRecaptcha(value);
-    };
-
-    const handleSubmit = (event) => {
-        event.preventDefault();
-        recoverPassword();
-    };
-
     return (
-        <View>
-            <h2>Recover password</h2>
-            <form onSubmit={handleSubmit}>
-                <div className="form-group">
-                    <label>Login</label>
-                    <input
-                        type="text"
-                        name="login"
-                        value={fieldState.login}
-                        onChange={handleInputChange}
-                        className="form-control"
-                        disabled={success}
-                        required
-                    />
-                </div>
-                <div className="form-group">
-                    <label>Email</label>
-                    <input
-                        type="email"
-                        name="email"
-                        value={fieldState.email}
-                        onChange={handleInputChange}
-                        className="form-control"
-                        disabled={success}
-                        required
-                    />
-                </div>
-                <div>
-                    <label>
-                        Please enter the information above to recover your
-                        password.
-                    </label>
-                </div>
-                {config.config["recaptcha_site_key"] ? (
-                    <ReCAPTCHA
-                        sitekey={config.config["recaptcha_site_key"]}
-                        onChange={onCaptchaChange}
-                    />
-                ) : (
-                    []
-                )}
-                <input
-                    type="submit"
-                    value="Submit"
-                    className="btn btn-primary"
-                    disabled={success}
-                />
-            </form>
-        </View>
+        <div className="user-password-recover">
+            <div className="user-password-recover__background" />
+            <div className="user-password-recover__container">
+                <View>
+                    <h2 className="text-center">Recover password</h2>
+                    <form onSubmit={handleSubmit(recoverPassword)}>
+                        <div className="form-group">
+                            <Label
+                                label="Login"
+                                required
+                                htmlFor={formFields.login}
+                            />
+                            <input
+                                {...register(formFields.login)}
+                                id={formFields.login}
+                                className={`form-control ${
+                                    errors.login ? "is-invalid" : ""
+                                }`}
+                            />
+                            <FormError errorField={errors.login} />
+                        </div>
+                        <div className="form-group">
+                            <Label
+                                label="Email"
+                                required
+                                htmlFor={formFields.email}
+                            />
+                            <input
+                                {...register(formFields.email)}
+                                id={formFields.email}
+                                className={`form-control ${
+                                    errors.email ? "is-invalid" : ""
+                                }`}
+                            />
+                            <FormError errorField={errors.email} />
+                        </div>
+                        <div>
+                            <p>
+                                Please enter the information above to recover
+                                your password.
+                            </p>
+                        </div>
+                        {config.config["recaptcha_site_key"] && (
+                            <>
+                                <ReCAPTCHA
+                                    ref={captchaRef}
+                                    style={{
+                                        display: "flex",
+                                        justifyContent: "center",
+                                        marginBottom: 12,
+                                    }}
+                                    sitekey={
+                                        config.config["recaptcha_site_key"]
+                                    }
+                                    onChange={(val) =>
+                                        setValue(formFields.recaptcha, val)
+                                    }
+                                />
+                                <div className="text-center">
+                                    <FormError errorField={errors.recaptcha} />
+                                </div>
+                            </>
+                        )}
+                        <div className="d-flex justify-content-between">
+                            <button
+                                className="btn btn-outline-primary btn-lg"
+                                onClick={() => redirectTo("/login")}
+                            >
+                                Back
+                            </button>
+                            <button
+                                type="submit"
+                                className="btn btn-primary btn-lg"
+                                disabled={loading}
+                            >
+                                <LoadingSpinner loading={loading} />
+                                Submit
+                            </button>
+                        </div>
+                    </form>
+                </View>
+            </div>
+        </div>
     );
 }
diff --git a/mwdb/web/src/components/UserRegister.jsx b/mwdb/web/src/components/UserRegister.jsx
index 7132515dd..34bdbaaf6 100644
--- a/mwdb/web/src/components/UserRegister.jsx
+++ b/mwdb/web/src/components/UserRegister.jsx
@@ -1,179 +1,269 @@
-import React, { useContext, useState } from "react";
-import { Link } from "react-router-dom";
+import React, { useContext, useRef, useState } from "react";
+import { useForm } from "react-hook-form";
+import { yupResolver } from "@hookform/resolvers/yup";
+import * as Yup from "yup";
 import ReCAPTCHA from "react-google-recaptcha";
 import { toast } from "react-toastify";
 
 import { api } from "@mwdb-web/commons/api";
 import { ConfigContext } from "@mwdb-web/commons/config";
-import { View, getErrorMessage } from "@mwdb-web/commons/ui";
+import {
+    View,
+    getErrorMessage,
+    Label,
+    FormError,
+    LoadingSpinner,
+} from "@mwdb-web/commons/ui";
+import { useNavRedirect } from "@mwdb-web/commons/hooks";
+
+const formFields = {
+    login: "login",
+    email: "email",
+    affiliation: "affiliation",
+    job_title: "job_title",
+    job_responsibilities: "job_responsibilities",
+    other_info: "other_info",
+    recaptcha: "recaptcha",
+};
+
+const validationSchema = Yup.object().shape({
+    [formFields.login]: Yup.string()
+        .required("Login is required")
+        .matches(
+            /[A-Za-z0-9_-]{1,32}/,
+            "Login must contain only letters, digits, '_'and '-' characters, max 32 characters allowed."
+        ),
+    [formFields.email]: Yup.string()
+        .required("Email is required")
+        .matches(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g, "Value is not email"),
+    [formFields.affiliation]: Yup.string().required("Affiliation is required"),
+    [formFields.job_title]: Yup.string().required("Job title is required"),
+
+    [formFields.job_responsibilities]: Yup.string().required(
+        "Job responsibilities is required"
+    ),
+    [formFields.other_info]: Yup.string().required(
+        "Other information is required"
+    ),
+    [formFields.recaptcha]: Yup.string()
+        .nullable()
+        .required("Recaptcha is required"),
+});
+
+const formOptions = {
+    resolver: yupResolver(validationSchema),
+    mode: "onSubmit",
+    reValidateMode: "onSubmit",
+    shouldFocusError: true,
+};
 
 export default function UserRegister() {
     const config = useContext(ConfigContext);
-    const [disable, setDisable] = useState(false);
-    const [recaptcha, setRecaptcha] = useState(null);
-    const [state, setState] = useState({
-        login: "",
-        email: "",
-        affiliation: "",
-        job_title: "",
-        job_responsibilities: "",
-        other_info: "",
-    });
+    const { redirectTo } = useNavRedirect();
+    const {
+        register,
+        handleSubmit,
+        setValue,
+        formState: { errors },
+    } = useForm(formOptions);
+
+    const [loading, setLoading] = useState(false);
+    const captchaRef = useRef(null);
 
-    async function registerUser() {
+    async function registerUser(values) {
         try {
-            setDisable(true);
-            let additional_info = `Affiliation: ${state.affiliation}, Job title: ${state.job_title}, ${state.job_responsibilities} ${state.other_info}`;
-            let response = await api.registerUser(
-                state.login,
-                state.email,
+            setLoading(true);
+            const additional_info = `Affiliation: ${values.affiliation}, Job title: ${values.job_title}, ${values.job_responsibilities} ${values.other_info}`;
+            const response = await api.registerUser(
+                values.login,
+                values.email,
                 additional_info,
-                recaptcha
+                values.recaptcha
             );
             toast(
                 `User ${response.data.login} registration requested. Account is
                         waiting for confirmation`,
                 { type: "success" }
             );
+            setLoading(false);
+            redirectTo("/login");
         } catch (e) {
             toast(getErrorMessage(e), {
                 type: "error",
             });
-            setDisable(false);
+            setLoading(false);
+        } finally {
+            captchaRef.current?.reset();
+            setValue(formFields.recaptcha, null);
         }
     }
 
-    const handleInputChange = (event) => {
-        const target = event.target;
-        const value =
-            target.type === "checkbox" ? target.checked : target.value;
-        const name = target.name;
-
-        setState({
-            ...state,
-            [name]: value,
-        });
-    };
-
-    const handleSubmit = (event) => {
-        event.preventDefault();
-        registerUser();
-    };
-
     return (
-        <View ident="userRegister">
-            <h2>Register user</h2>
-            <p>
-                Provided data are needed for vetting process. Keep in mind that
-                all submissions are reviewed manually, so the approval process
-                can take a few days. Before filling this form, make sure you
-                have read our <Link to="/terms/en">Terms of use</Link>.
-            </p>
-            <form onSubmit={handleSubmit}>
-                <div className="form-group">
-                    <label>Login</label>
-                    <input
-                        type="text"
-                        name="login"
-                        value={state.login}
-                        onChange={handleInputChange}
-                        className="form-control"
-                        required
-                        pattern="[A-Za-z0-9_-]{1,32}"
-                        disabled={disable}
-                    />
-                    <div className="form-hint">
-                        Login must contain only letters, digits, '_' and '-'
-                        characters, max 32 characters allowed.
-                    </div>
-                </div>
-                <div className="form-group">
-                    <label>Business e-mail</label>
-                    <input
-                        type="email"
-                        name="email"
-                        value={state.email}
-                        onChange={handleInputChange}
-                        className="form-control"
-                        required
-                        disabled={disable}
-                    />
-                </div>
-                <b>Additional information:</b>
-                <div className="form-group">
-                    <label>Affiliation</label>
-                    <input
-                        type="text"
-                        name="affiliation"
-                        value={state.affiliation}
-                        onChange={handleInputChange}
-                        className="form-control"
-                        required
-                        disabled={disable}
-                    />
-                    <div className="form-hint">
-                        Provide name of company or university
-                    </div>
-                </div>
-                <div className="form-group">
-                    <label>Job Title</label>
-                    <input
-                        type="text"
-                        name="job_title"
-                        value={state.job_title}
-                        onChange={handleInputChange}
-                        className="form-control"
-                        required
-                        disabled={disable}
-                    />
-                    <div className="form-hint">Provide your job title</div>
-                </div>
-                <div className="form-group">
-                    <label>Job Responsibilities</label>
-                    <input
-                        type="text"
-                        name="job_responsibilities"
-                        value={state.job_responsibilities}
-                        onChange={handleInputChange}
-                        className="form-control"
-                        required
-                        disabled={disable}
-                    />
-                    <div className="form-hint">
-                        Provide your job responsibilities and experience in the
-                        field of malware analysis
-                    </div>
-                </div>
-                <div className="form-group">
-                    <label>Other information</label>
-                    <textarea
-                        name="other_info"
-                        value={state.other_info}
-                        onChange={handleInputChange}
-                        className="form-control"
-                        required
-                        disabled={disable}
-                    />
-                    <div className="form-hint">
-                        Provide additional information e.g. Twitter handle,
-                        invitation info, blog URL etc.
-                    </div>
-                </div>
-                {config.config["recaptcha_site_key"] ? (
-                    <ReCAPTCHA
-                        sitekey={config.config["recaptcha_site_key"]}
-                        onChange={(value) => setRecaptcha(value)}
-                    />
-                ) : (
-                    []
-                )}
-                <input
-                    type="submit"
-                    value="Submit"
-                    className="btn btn-primary"
-                    disabled={disable}
-                />
-            </form>
-        </View>
+        <div className="register-form">
+            <div className="register-form__background" />
+            <div className="register-form__container">
+                <View ident="userRegister">
+                    <h2 className="text-center">Register user</h2>
+                    <p className="text-center">
+                        Provided data are needed for vetting process. Keep in
+                        mind that all submissions are reviewed manually, so the
+                        approval process can take a few days.
+                    </p>
+                    <form onSubmit={handleSubmit(registerUser)}>
+                        <div className="row">
+                            <div className="form-group col-md">
+                                <Label
+                                    label="Login"
+                                    required
+                                    htmlFor={formFields.login}
+                                />
+                                <input
+                                    {...register(formFields.login)}
+                                    id={formFields.login}
+                                    className={`form-control ${
+                                        errors.login ? "is-invalid" : ""
+                                    }`}
+                                />
+
+                                <FormError errorField={errors.login} />
+                            </div>
+                            <div className="form-group col-md">
+                                <Label
+                                    label="Business e-mail"
+                                    required
+                                    htmlFor={formFields.email}
+                                />
+                                <input
+                                    {...register(formFields.email)}
+                                    id={formFields.email}
+                                    className={`form-control ${
+                                        errors.email ? "is-invalid" : ""
+                                    }`}
+                                />
+                                <FormError errorField={errors.email} />
+                            </div>
+                        </div>
+                        <b>Additional information:</b>
+                        <div className="row">
+                            <div className="form-group col-md">
+                                <Label
+                                    label="Affiliation"
+                                    required
+                                    htmlFor={formFields.affiliation}
+                                />
+                                <input
+                                    {...register(formFields.affiliation)}
+                                    id={formFields.affiliation}
+                                    className={`form-control ${
+                                        errors.affiliation ? "is-invalid" : ""
+                                    }`}
+                                />
+                                <FormError errorField={errors.affiliation} />
+                                <div className="form-hint">
+                                    Provide name of company or university
+                                </div>
+                            </div>
+                            <div className="form-group col-md">
+                                <Label
+                                    label="Job Title"
+                                    required
+                                    htmlFor={formFields.job_title}
+                                />
+                                <input
+                                    {...register(formFields.job_title)}
+                                    id={formFields.job_title}
+                                    className={`form-control ${
+                                        errors.job_title ? "is-invalid" : ""
+                                    }`}
+                                />
+                                <FormError errorField={errors.job_title} />
+                                <div className="form-hint">
+                                    Provide your job title
+                                </div>
+                            </div>
+                        </div>
+                        <div className="form-group">
+                            <Label
+                                label="Job Responsibilities"
+                                required
+                                htmlFor={formFields.job_responsibilities}
+                            />
+                            <input
+                                {...register(formFields.job_responsibilities)}
+                                id={formFields.job_responsibilities}
+                                className={`form-control ${
+                                    errors.job_responsibilities
+                                        ? "is-invalid"
+                                        : ""
+                                }`}
+                            />
+                            <FormError
+                                errorField={errors.job_responsibilities}
+                            />
+                            <div className="form-hint">
+                                Provide your job responsibilities and experience
+                                in the field of malware analysis
+                            </div>
+                        </div>
+                        <div className="form-group">
+                            <Label
+                                label="Other information"
+                                required
+                                htmlFor={formFields.other_info}
+                            />
+                            <textarea
+                                {...register(formFields.other_info)}
+                                id={formFields.other_info}
+                                className={`form-control ${
+                                    errors.other_info ? "is-invalid" : ""
+                                }`}
+                            />
+                            <FormError errorField={errors.other_info} />
+                            <div className="form-hint">
+                                Provide additional information e.g. Twitter
+                                handle, invitation info, blog URL etc.
+                            </div>
+                        </div>
+                        {config.config["recaptcha_site_key"] && (
+                            <>
+                                <ReCAPTCHA
+                                    ref={captchaRef}
+                                    style={{
+                                        display: "flex",
+                                        justifyContent: "center",
+                                        marginBottom: 12,
+                                    }}
+                                    sitekey={
+                                        config.config["recaptcha_site_key"]
+                                    }
+                                    onChange={(val) =>
+                                        setValue(formFields.recaptcha, val)
+                                    }
+                                />
+                                <div className="text-center">
+                                    <FormError errorField={errors.recaptcha} />
+                                </div>
+                            </>
+                        )}
+                        <div className="d-flex justify-content-between">
+                            <button
+                                className="btn btn-outline-primary btn-lg"
+                                onClick={() => redirectTo("/login")}
+                            >
+                                Back
+                            </button>
+                            <button
+                                type="submit"
+                                className="btn btn-primary btn-lg"
+                                disabled={loading}
+                            >
+                                <LoadingSpinner loading={loading} />
+                                Submit
+                            </button>
+                        </div>
+                    </form>
+                </View>
+            </div>
+        </div>
     );
 }
diff --git a/mwdb/web/src/styles/index.css b/mwdb/web/src/styles/index.css
index 228458d3c..6eef71dfa 100644
--- a/mwdb/web/src/styles/index.css
+++ b/mwdb/web/src/styles/index.css
@@ -168,6 +168,11 @@ pre.attribute-object {
     display: inline-block;
 }
 
+.required:after {
+    content: "*";
+    color: red;
+}
+
 /***
  User login CSS
  ***/
@@ -202,6 +207,62 @@ div.user-login .container-fluid {
     }
 }
 
+/* Register form */
+.register-form__container {
+    background-color: white;
+    padding: 3rem;
+    border-radius: 10px;
+    max-width: 700px;
+    margin: 2rem auto;
+}
+@media (max-width: 768px) {
+    .register-form__container {
+        margin: 0 auto;
+        padding: 1rem;
+    }
+}
+
+@media (min-width: 768px) {
+    .register-form__background {
+        position: absolute;
+        top: 0;
+        left: 0;
+        box-sizing: border-box;
+        background-color: #f2f2f2;
+        z-index: -1;
+        height: 100vh;
+        width: 100vw;
+    }
+}
+
+/* Recover password form */
+.user-password-recover__container {
+    background-color: white;
+    padding: 3rem;
+    border-radius: 10px;
+    max-width: 450px;
+    margin: 2rem auto;
+}
+@media (max-width: 576px) {
+    .user-password-recover__container {
+        margin: 0 auto;
+        padding: 1rem;
+    }
+}
+
+@media (min-width: 576px) {
+    .user-password-recover__background {
+        position: absolute;
+        top: 0;
+        left: 0;
+        box-sizing: border-box;
+        background-color: #f2f2f2;
+        z-index: -1;
+        height: 100vh;
+        width: 100vw;
+    }
+}
+
 /***
 	Material Design Switches for Bootstrap 4 and Material Design Bootstrap (MDB)
 	by djibe


Test patch:
diff --git a/tests/frontend/cypress/integration/blob.js b/tests/frontend/cypress/integration/blob.js
index 3787acbbb..6f98642f9 100644
--- a/tests/frontend/cypress/integration/blob.js
+++ b/tests/frontend/cypress/integration/blob.js
@@ -35,7 +35,7 @@ describe("Blob view test - mwdb-core", function () {
     cy.contains("Blobs").click({ timeout: 10000 });
 
     cy.get("@blobId").then((blobId) => {
-      cy.get('.d-none a[href*="' + blobId + '"] > div').click();
+      cy.get('.d-none a[href*="' + blobId + '"] > div').click({ force: true });
       cy.server()
         .route("GET", "/api/blob/" + blobId)
         .as("dataGetFirst");
@@ -46,7 +46,7 @@ describe("Blob view test - mwdb-core", function () {
       expect(text).to.include(blobContent);
     });
 
-    cy.contains("Details").click();
+    cy.contains("Details").click({ force: true });
     cy.contains("Blob name");
     cy.contains("some.blob");
     cy.contains("Blob size");
@@ -55,7 +55,7 @@ describe("Blob view test - mwdb-core", function () {
     cy.contains("First seen");
     cy.contains("Last seen");
 
-    cy.get("a.nav-link").contains("Relations").click();
+    cy.get("a.nav-link").contains("Relations").click({ force: true });
     cy.get("g[class='node expanded-node']");
 
     cy.visit("/blob/fake");
@@ -63,6 +63,6 @@ describe("Blob view test - mwdb-core", function () {
       "The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again."
     );
 
-    cy.contains("Logout").click();
+    cy.contains("Logout").click({ force: true });
   });
 });
diff --git a/tests/frontend/cypress/integration/config.js b/tests/frontend/cypress/integration/config.js
index af8b91232..465c3b436 100644
--- a/tests/frontend/cypress/integration/config.js
+++ b/tests/frontend/cypress/integration/config.js
@@ -36,10 +36,12 @@ describe("Config view test - mwdb-core", function () {
 
     browserLogin(Cypress.env("user"), Cypress.env("password"));
 
-    cy.contains("Configs").click();
+    cy.contains("Configs").click({ timeout: 10000 });
 
     cy.get("@configId").then((configId) => {
-      cy.get('.d-none a[href*="'+configId+'"] > div').click();
+      cy.get('.d-none a[href*="' + configId + '"] > div').click({
+        force: true,
+      });
     });
 
     cy.contains("Family");
@@ -54,10 +56,10 @@ describe("Config view test - mwdb-core", function () {
     cy.contains("test");
     cy.contains("Upload time");
 
-    cy.get("a").contains("Relations").click()
+    cy.get("a").contains("Relations").click({ force: true });
     cy.get("g[class='node expanded-node']");
 
-    cy.get("a").contains("Preview").click()
+    cy.get("a").contains("Preview").click({ force: true });
     cy.get("div[class='ace_line']");
 
     cy.visit("/config/fake");
@@ -65,6 +67,6 @@ describe("Config view test - mwdb-core", function () {
       "The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again."
     );
 
-    cy.contains("Logout").click();
+    cy.contains("Logout").click({ force: true });
   });
 });
diff --git a/tests/frontend/cypress/integration/login.js b/tests/frontend/cypress/integration/login.js
index f6a5cc23b..6a246020f 100644
--- a/tests/frontend/cypress/integration/login.js
+++ b/tests/frontend/cypress/integration/login.js
@@ -6,6 +6,6 @@ describe("Login test - mwdb-core", function () {
 
     browserLogin(Cypress.env("user"), Cypress.env("password"));
 
-    cy.contains("Logout").click();
+    cy.contains("Logout").click({ force: true });
   });
 });
diff --git a/tests/frontend/cypress/integration/sample.js b/tests/frontend/cypress/integration/sample.js
index 73268ffd2..19d7d7305 100644
--- a/tests/frontend/cypress/integration/sample.js
+++ b/tests/frontend/cypress/integration/sample.js
@@ -1,78 +1,74 @@
 import { requestLogin, browserLogin } from "./util";
 
 describe("Sample view test - mwdb-core", function () {
-    it("Sample view test - existent and non-existent md5 and sha256 hashes", function () {
-        requestLogin(Cypress.env("user"), Cypress.env("password"));
+  it("Sample view test - existent and non-existent md5 and sha256 hashes", function () {
+    requestLogin(Cypress.env("user"), Cypress.env("password"));
 
-        const fileName = "TEST";
-        const method = "POST";
-        const apiUrl = "/api/file";
-        const fileType = "text/plain";
+    const fileName = "TEST";
+    const method = "POST";
+    const apiUrl = "/api/file";
 
-        const addedFile = new Cypress.Promise((resolve) => {
-            cy.get("@token").then((token) => {
-                cy.fixture(fileName).then((bin) => {
-                    return Cypress.Blob.binaryStringToBlob(bin, fileType).then(
-                        (blob) => {
-                            const formData = new FormData();
-                            formData.set("file", blob, fileName);
+    const fileType = "text/plain";
 
-                            cy.formRequest(
-                                method,
-                                apiUrl,
-                                formData,
-                                token
-                            ).then((response) => {
-                                expect(response.status).to.eq(200);
-                                resolve(response.response.body);
-                            });
-                        }
-                    );
-                });
+    const addedFile = new Cypress.Promise((resolve) => {
+      cy.get("@token").then((token) => {
+        cy.fixture(fileName).then((bin) => {
+          return Cypress.Blob.binaryStringToBlob(bin, fileType).then((blob) => {
+            const formData = new FormData();
+            formData.set("file", blob, fileName);
+
+            cy.formRequest(method, apiUrl, formData, token).then((response) => {
+              expect(response.status).to.eq(200);
+              resolve(response.response.body);
             });
+          });
         });
+      });
+    });
 
-        addedFile.then((fileData) => {
-            cy.visit("/");
-            browserLogin(Cypress.env("user"), Cypress.env("password"));
+    addedFile.then((fileData) => {
+      cy.visit("/");
+      browserLogin(Cypress.env("user"), Cypress.env("password"));
 
-            cy.contains("Samples").click();
-            cy.get('.d-none a[href*="' + fileData.md5 + '"] > div').click();
-            cy.contains(fileData.md5);
-            cy.contains("File name");
-            cy.contains("TEST");
-            cy.contains("12");
-            cy.contains("File type");
-            cy.contains("ASCII text");
-            cy.contains("sha1");
-            cy.contains("sha256");
-            cy.contains("sha512");
-            cy.contains("crc32");
-            cy.contains("ssdeep");
-            cy.contains("Upload time");
+      cy.contains("Samples").click({ timeout: 10000 });
+      cy.get('.d-none a[href*="' + fileData.md5 + '"] > div').click({
+        force: true,
+      });
+      cy.contains(fileData.md5);
+      cy.contains("File name");
+      cy.contains("TEST");
+      cy.contains("12");
+      cy.contains("File type");
+      cy.contains("ASCII text");
+      cy.contains("sha1");
+      cy.contains("sha256");
+      cy.contains("sha512");
+      cy.contains("crc32");
+      cy.contains("ssdeep");
+      cy.contains("Upload time");
 
-            cy.get("a").contains("Relations").click();
-            cy.get("g[class='node expanded-node']");
+      cy.get("a").contains("Relations").click({ force: true });
+      cy.get("g[class='node expanded-node']");
 
-            cy.get("a").contains("Preview").click();
-            cy.get("div[class='ace_line']");
+      cy.get("a").contains("Preview").click({ force: true });
+      cy.get("div[class='ace_line']");
 
-            cy.contains("Samples").click();
+      cy.contains("Samples").click({ force: true });
 
-            cy.get('.d-none a[href*="' + fileData.sha256 + '"] > div')
-                .should("be.visible")
-                .click();
+      cy.get('.d-none a[href*="' + fileData.sha256 + '"] > div')
+        .should("be.visible")
+        .click({ force: true });
 
-            cy.contains(fileData.sha256);
+      cy.contains(fileData.sha256);
 
-            cy.visit("/file/fake");
-            cy.contains(
-                "The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again."
-            );
-            cy.visit("/file/03040506708090aabbccddeeff112233");
-            cy.contains("Object not found");
+      cy.visit("/file/fake");
+      cy.contains(
+        "The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again."
+      );
+      cy.visit("/file/03040506708090aabbccddeeff112233");
+      cy.contains("Object not found");
 
-            cy.contains("Logout").click();
-        });
+      cy.contains("Logout").click({ force: true });
     });
+  });
 });