Merge pull request #3251 from bluewave-labs/feat/v2-auth-login
feat: v2 auth login
This commit is contained in:
20 files changed
+465
-236
No files matched your search
@@ -1,4 +1,6 @@
|
||||
import Logo from "@/assets/icons/checkmate-icon.svg?react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Link from "@mui/material/Link";
|
||||
import {
|
||||
@@ -9,6 +11,7 @@ import {
|
||||
} from "@/Components/v2/design-elements/Fallback";
|
||||
import { Breadcrumb } from "@/Components/v2/design-elements/Breadcrumb";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { HeaderAuthControls } from "@/Pages/Auth/components/HeaderAuthControls";
|
||||
|
||||
import type { StackProps } from "@mui/material/Stack";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
@@ -201,3 +204,61 @@ export const MonitorBasePageWithStates = ({
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
interface BaseAuthPageProps extends React.PropsWithChildren, StackProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
export const BaseAuthPage = ({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
...props
|
||||
}: BaseAuthPageProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BasePage
|
||||
breadcrumbOverride={[]}
|
||||
gap={theme.spacing(8)}
|
||||
alignItems={"center"}
|
||||
justifyContent={"center"}
|
||||
minHeight="100vh"
|
||||
position={"relative"}
|
||||
{...props}
|
||||
>
|
||||
<HeaderAuthControls
|
||||
hideLogo
|
||||
py={theme.spacing(4)}
|
||||
position={"absolute"}
|
||||
top={0}
|
||||
left={0}
|
||||
/>
|
||||
<Box width={{ xs: 60, sm: 70, md: 80 }}>
|
||||
<Logo
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
/>
|
||||
</Box>
|
||||
<Stack alignItems={"center"}>
|
||||
<Typography
|
||||
variant="h1"
|
||||
mb={theme.spacing(2)}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="h1">{subtitle}</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.spacing(8)}
|
||||
width={{
|
||||
xs: "80%",
|
||||
md: "25%",
|
||||
lg: "15%",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import type { StackProps } from "@mui/material/Stack";
|
||||
import Link from "@mui/material/Link";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
interface TextLinkProps extends StackProps {
|
||||
text: string;
|
||||
linkText: string;
|
||||
href: string;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
export const TextLink = ({
|
||||
text,
|
||||
linkText,
|
||||
href,
|
||||
target = "_self",
|
||||
...props
|
||||
}: TextLinkProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
{...props}
|
||||
>
|
||||
<Typography>{text}</Typography>
|
||||
<Link
|
||||
color="primary"
|
||||
to={href}
|
||||
component={RouterLink}
|
||||
target={target}
|
||||
>
|
||||
{linkText}
|
||||
</Link>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -18,3 +18,4 @@ export * from "./BaseChart";
|
||||
export * from "./Gauge";
|
||||
export * from "./Tabs";
|
||||
export * from "./SplitBox";
|
||||
export * from "./TextLink";
|
||||
@@ -0,0 +1,100 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { Select } from "@/Components/v2/inputs";
|
||||
import "flag-icons/css/flag-icons.min.css";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { setLanguage } from "@/Features/UI/uiSlice";
|
||||
import type { RootState } from "@/Types/state";
|
||||
|
||||
const langMap: Record<string, string> = {
|
||||
cs: "cz",
|
||||
ja: "jp",
|
||||
uk: "ua",
|
||||
vi: "vn",
|
||||
};
|
||||
|
||||
export const LanguageSelector = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { language = "en" } = useSelector((state: RootState) => state.ui);
|
||||
const dispatch = useDispatch();
|
||||
const handleChange = (event: any) => {
|
||||
const newLang = event.target.value;
|
||||
dispatch(setLanguage(newLang));
|
||||
};
|
||||
|
||||
const languages = Object.keys(i18n.options.resources || {});
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={language}
|
||||
onChange={handleChange}
|
||||
size="small"
|
||||
// sx={{
|
||||
// minWidth: 80,
|
||||
// "& .MuiSelect-select": {
|
||||
// display: "flex",
|
||||
// alignItems: "center",
|
||||
// justifyContent: "center",
|
||||
// },
|
||||
// "& .MuiSelect-icon": {
|
||||
// alignSelf: "center",
|
||||
// },
|
||||
// }}
|
||||
>
|
||||
{languages.map((lang) => {
|
||||
let parsedLang = lang === "en" ? "gb" : lang;
|
||||
|
||||
if (parsedLang.includes("-")) {
|
||||
parsedLang = parsedLang.split("-")[1].toLowerCase();
|
||||
}
|
||||
|
||||
parsedLang = langMap[parsedLang] || parsedLang;
|
||||
|
||||
const flag = parsedLang ? `fi fi-${parsedLang}` : null;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={lang}
|
||||
value={lang}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={theme.spacing(2)}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{flag && <span className={flag} />}
|
||||
</Box>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ textTransform: "uppercase" }}
|
||||
>
|
||||
{lang}
|
||||
</Box>
|
||||
</Stack>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSelector;
|
||||
@@ -0,0 +1,48 @@
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
|
||||
import { setMode } from "@/Features/UI/uiSlice.js";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import type { RootState } from "@/Types/state";
|
||||
|
||||
export const SwitchTheme = () => {
|
||||
const mode = useSelector((state: RootState) => state.ui.mode);
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
|
||||
const handleChange = () => {
|
||||
dispatch(setMode(mode === "light" ? "dark" : "light"));
|
||||
};
|
||||
return (
|
||||
<IconButton
|
||||
id="theme-toggle"
|
||||
onClick={handleChange}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
"& svg": {
|
||||
transition: "stroke 0.2s ease",
|
||||
},
|
||||
"&:hover svg path, &:hover svg line, &:hover svg polyline, &:hover svg rect, &:hover svg circle":
|
||||
{
|
||||
stroke: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{mode === "light" ? (
|
||||
<Moon
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
) : (
|
||||
<Sun
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
@@ -16,3 +16,5 @@ export * from "./ImageUpload";
|
||||
export * from "./ColorPicker";
|
||||
export { DatePickerComponent as DatePicker } from "./DatePicker";
|
||||
export { TimePickerComponent as TimePicker } from "./TimePicker";
|
||||
export * from "./LanguageSelector";
|
||||
export * from "./SwitchTheme";
|
||||
@@ -196,6 +196,13 @@ const authSlice = createSlice({
|
||||
state.success = true;
|
||||
state.msg = "Logged out successfully";
|
||||
},
|
||||
setAuthState: (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.authToken = action.payload.data.token;
|
||||
state.user = action.payload.data.user;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Register thunk
|
||||
@@ -249,4 +256,4 @@ const authSlice = createSlice({
|
||||
});
|
||||
|
||||
export default authSlice.reducer;
|
||||
export const { clearAuthState } = authSlice.actions;
|
||||
export const { clearAuthState, setAuthState } = authSlice.actions;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useMemo } from "react";
|
||||
import { loginSchema, type LoginFormData } from "@/Validation/login";
|
||||
|
||||
export const useLoginForm = () => {
|
||||
return useMemo(() => {
|
||||
const defaults: LoginFormData = {
|
||||
email: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
return { schema: loginSchema, defaults };
|
||||
}, []);
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from "react";
|
||||
import AppBar from "@/Components/v1/Common/AppBar.jsx";
|
||||
import Footer from "@/Components/v1/Common/Footer.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const AboutUs = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AppBar />
|
||||
<h1>{t("aboutus")}</h1>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutUs;
|
||||
@@ -1,22 +0,0 @@
|
||||
// Hook to avoid double submits and manage loading state
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
const useLoadingSubmit = () => {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const executeSubmit = useCallback(
|
||||
async (submitFunction) => {
|
||||
if (submitting) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
return await submitFunction();
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
[submitting]
|
||||
);
|
||||
|
||||
return { submitting, executeSubmit };
|
||||
};
|
||||
|
||||
export default useLoadingSubmit;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const useLoginForm = () => {
|
||||
const [form, setForm] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
const onChange = (e) => {
|
||||
let { name, value } = e.target;
|
||||
if (name === "email") {
|
||||
value = value.toLowerCase();
|
||||
}
|
||||
const updatedForm = { ...form, [name]: value };
|
||||
setForm(updatedForm);
|
||||
};
|
||||
return [form, onChange];
|
||||
};
|
||||
|
||||
export default useLoginForm;
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { login } from "../../../../Features/Auth/authSlice.js";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createToast } from "../../../../Utils/toastUtils.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const useLoginSubmit = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleLoginSubmit = async (form, setErrors) => {
|
||||
const action = await dispatch(login(form));
|
||||
if (action.payload.success) {
|
||||
navigate("/uptime");
|
||||
createToast({
|
||||
body: t("auth.login.toasts.success"),
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
if (action.payload.msg === "Incorrect password")
|
||||
setErrors({
|
||||
password: t("auth.login.errors.password.incorrect"),
|
||||
});
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: t("auth.login.toasts.incorrectPassword"),
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: t("common.toasts.unknownError"),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
return [handleLoginSubmit];
|
||||
};
|
||||
|
||||
export default useLoginSubmit;
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { loginCredentials } from "../../../../Validation/validation.js";
|
||||
|
||||
const useValidateLoginForm = () => {
|
||||
const [errors, setErrors] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
const validateField = (name, value) => {
|
||||
const { error } = loginCredentials.validate({ [name]: value });
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[name]: error?.details?.[0]?.message || "",
|
||||
}));
|
||||
};
|
||||
const validateForm = (form) => {
|
||||
const { error } = loginCredentials.validate(form, { abortEarly: false });
|
||||
if (error) {
|
||||
const formErrors = {};
|
||||
for (const err of error.details) {
|
||||
formErrors[err.path[0]] = err.message;
|
||||
}
|
||||
setErrors(formErrors);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return [errors, setErrors, validateField, validateForm];
|
||||
};
|
||||
|
||||
export default useValidateLoginForm;
|
||||
@@ -1,102 +0,0 @@
|
||||
// Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Button from "@mui/material/Button";
|
||||
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
|
||||
import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
|
||||
import TextLink from "@/Components/v1/TextLink/index.jsx";
|
||||
import AuthPageWrapper from "../components/AuthPageWrapper.jsx";
|
||||
// Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useLoginForm from "./hooks/useLoginForm.jsx";
|
||||
import useValidateLoginForm from "./hooks/useValidateLoginForm.jsx";
|
||||
import useLoginSubmit from "./hooks/useLoginSubmit.jsx";
|
||||
import useLoadingSubmit from "./hooks/useLoadingSubmit.jsx";
|
||||
|
||||
const Login = () => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [form, onChange] = useLoginForm();
|
||||
const [errors, setErrors, validateField, validateForm] = useValidateLoginForm();
|
||||
const [handleLoginSubmit] = useLoginSubmit();
|
||||
const { submitting, executeSubmit } = useLoadingSubmit();
|
||||
|
||||
// Handlers
|
||||
const handleChange = (e) => {
|
||||
onChange(e);
|
||||
validateField(e.target.name, e.target.value);
|
||||
};
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const isValid = validateForm(form);
|
||||
if (!isValid) return;
|
||||
await executeSubmit(() => handleLoginSubmit(form, setErrors));
|
||||
};
|
||||
return (
|
||||
<AuthPageWrapper
|
||||
welcome={t("auth.login.welcome")}
|
||||
heading={t("auth.login.heading")}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
width="100%"
|
||||
padding={theme.spacing(8)}
|
||||
gap={theme.spacing(12)}
|
||||
onSubmit={onSubmit}
|
||||
sx={{
|
||||
width: {
|
||||
sm: "80%",
|
||||
md: "70%",
|
||||
lg: "65%",
|
||||
xl: "65%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
type="email"
|
||||
name="email"
|
||||
label={t("auth.common.inputs.email.label")}
|
||||
placeholder={t("auth.common.inputs.email.placeholder")}
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
|
||||
/>
|
||||
<TextInput
|
||||
type="password"
|
||||
name="password"
|
||||
label={t("auth.common.inputs.password.label")}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password ? true : false}
|
||||
helperText={errors.password ? t(errors.password) : ""} // Localization keys are in validation.js
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
type="submit"
|
||||
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
|
||||
loading={submitting}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Stack>
|
||||
<TextLink
|
||||
text={t("auth.login.links.forgotPassword")}
|
||||
linkText={t("auth.login.links.forgotPasswordLink")}
|
||||
href="/forgot-password"
|
||||
/>
|
||||
<TextLink
|
||||
text={t("auth.login.links.register")}
|
||||
linkText={t("auth.login.links.registerLink")}
|
||||
href="/register"
|
||||
/>
|
||||
</AuthPageWrapper>
|
||||
);
|
||||
};
|
||||
export default Login;
|
||||
@@ -0,0 +1,83 @@
|
||||
import { BaseAuthPage } from "@/Components/v2/design-elements";
|
||||
import { Button, TextField } from "@/Components/v2/inputs";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js";
|
||||
import { useLoginForm } from "@/Hooks/useLoginForm";
|
||||
import type { LoginFormData } from "@/Validation/login";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { setAuthState } from "@/Features/Auth/authSlice";
|
||||
import { usePost } from "@/Hooks/UseApi";
|
||||
|
||||
const LoginPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { post, loading } = usePost();
|
||||
|
||||
const { schema, defaults } = useLoginForm();
|
||||
|
||||
const { control, handleSubmit } = useForm<LoginFormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: defaults,
|
||||
});
|
||||
|
||||
const onSubmit = async (data: LoginFormData) => {
|
||||
if (loading) return;
|
||||
|
||||
const result = await post("/auth/login", data);
|
||||
|
||||
if (result?.success) {
|
||||
dispatch(setAuthState(result));
|
||||
navigate("/uptime");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseAuthPage
|
||||
component="form"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
title={t("pages.auth.login.title")}
|
||||
subtitle={t("pages.auth.login.subtitle")}
|
||||
>
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("pages.auth.common.form.option.email.label")}
|
||||
placeholder={t("pages.auth.common.form.option.email.placeholder")}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error ? t(fieldState.error.message ?? "") : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="password"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="password"
|
||||
fieldLabel={t("pages.auth.common.form.option.password.label")}
|
||||
placeholder={t("pages.auth.common.form.option.password.placeholder")}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error ? t(fieldState.error.message ?? "") : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
type="submit"
|
||||
loading={loading}
|
||||
>
|
||||
{t("pages.auth.login.submit")}
|
||||
</Button>
|
||||
</BaseAuthPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
@@ -0,0 +1,53 @@
|
||||
import Stack, { type StackProps } from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Logo from "@/assets/icons/checkmate-icon.svg?react";
|
||||
import { LanguageSelector, SwitchTheme } from "@/Components/v2/inputs";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface HeaderAuthControlsProps extends StackProps {
|
||||
hideLogo?: boolean;
|
||||
}
|
||||
|
||||
export const HeaderAuthControls = ({
|
||||
hideLogo = false,
|
||||
...stackProps
|
||||
}: HeaderAuthControlsProps) => {
|
||||
// Hooks
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
width={"100%"}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
{...stackProps}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
{!hideLogo && (
|
||||
<>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={theme.spacing(2)}
|
||||
alignItems="center"
|
||||
>
|
||||
<LanguageSelector />
|
||||
<SwitchTheme />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import { Navigate, Route, Routes as LibRoutes } from "react-router";
|
||||
import HomeLayout from "@/Components/v1/Layouts/HomeLayout";
|
||||
import NotFound from "../Pages/NotFound/index.jsx";
|
||||
// Auth
|
||||
import AuthLogin from "../Pages/Auth/Login/index.jsx";
|
||||
import AuthLogin from "../Pages/Auth/Login";
|
||||
import AuthRegister from "../Pages/Auth/Register/index.jsx";
|
||||
import AuthForgotPassword from "../Pages/Auth/ForgotPassword.jsx";
|
||||
import AuthCheckEmail from "../Pages/Auth/CheckEmail.jsx";
|
||||
@@ -360,7 +360,13 @@ const Routes = () => {
|
||||
|
||||
<Route
|
||||
path="/login"
|
||||
element={<AuthLogin />}
|
||||
element={
|
||||
<>
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<AuthLogin />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -42,7 +42,9 @@ export const initApiClient = (store: StoreType): void => {
|
||||
const onSuccess = (response: AxiosResponse) => response;
|
||||
const onError = (error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
window.location.href = "/login";
|
||||
if (window.location.pathname !== "/login") {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, "auth.common.inputs.email.errors.empty")
|
||||
.email("auth.common.inputs.email.errors.invalid")
|
||||
.transform((val) => val.toLowerCase().trim()),
|
||||
password: z.string().min(1, "auth.common.inputs.password.errors.empty"),
|
||||
});
|
||||
|
||||
export type LoginFormData = z.infer<typeof loginSchema>;
|
||||
@@ -498,6 +498,37 @@
|
||||
"now": "Now",
|
||||
"os": "OS",
|
||||
"pages": {
|
||||
"auth": {
|
||||
"common": {
|
||||
"form": {
|
||||
"option": {
|
||||
"email": {
|
||||
"label": "Email",
|
||||
"placeholder": "me@example.com"
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"placeholder": "********"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"title": "Welcome back to Checkmate!",
|
||||
"subtitle": "Log in to continue",
|
||||
"submit": "Login",
|
||||
"links": {
|
||||
"forgotPassword": {
|
||||
"text": "Forgot password?",
|
||||
"linkText": "Reset password"
|
||||
},
|
||||
"register": {
|
||||
"text": "Don't have an account?",
|
||||
"linkText": "Register here"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"checks": {
|
||||
"selects": {
|
||||
"monitor": {
|
||||
|
||||
Reference in new issue
Block a user