Add authentification

This commit is contained in:
2026-02-17 00:54:36 +01:00
parent ab98ba81c8
commit a8c8c489da
31 changed files with 1118 additions and 451 deletions

View File

@@ -0,0 +1,20 @@
import { useCurrentUser } from "@/services/api";
import { Group, Loader } from "@mantine/core";
import { Navigate, Outlet } from "react-router";
export function Auth() {
const { data: userLogged, isLoading } = useCurrentUser();
if (!userLogged && isLoading)
return (
<Group align="center" justify="center" h="80vh" w="100%">
<Loader color="pink" />
</Group>
);
if (!userLogged?.logged) {
return <Navigate to="/" replace />;
}
return <Outlet />;
}

View File

@@ -1,50 +1,57 @@
import { Button, Group, Modal, TextInput, Title, type ModalBaseProps } from "@mantine/core";
import { Button, Group, Modal, Select, type ModalBaseProps } from "@mantine/core";
import { t } from "@/config/i18n";
import { useForm } from "@mantine/form";
import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
import { type Contract, type ContractInputs } from "@/services/resources/contracts";
import { IconCancel, IconDownload } from "@tabler/icons-react";
import { useGetForms } from "@/services/api";
import { useMemo } from "react";
export type ContractModalProps = ModalBaseProps & {
currentContract?: Contract;
handleSubmit: (contract: ContractInputs, id?: number) => void;
handleSubmit: (id: number) => void;
};
export function ContractModal({
opened,
onClose,
currentContract,
handleSubmit,
}: ContractModalProps) {
const form = useForm<ContractInputs>({
// initialValues: {
// firstname: currentContract?.firstname ?? "",
// lastname: currentContract?.lastname ?? "",
// email: currentContract?.email ?? "",
// },
// validate: {
// firstname: (value) =>
// !value ? `${t("name", { capfirst: true })} ${t("is required")}` : null,
// email: (value) =>
// !value ? `${t("email", { capfirst: true })} ${t("is required")}` : null,
// },
export type ContractDownloadInputs = {
form_id: number;
};
export function ContractModal({ opened, onClose, handleSubmit }: ContractModalProps) {
const { data: allForms } = useGetForms();
const form = useForm({
initialValues: {
form_id: null,
},
validate: {
form_id: (value) =>
!value ? `${t("a form", { capfirst: true })} ${t("is required")}` : null,
},
});
const formSelect = useMemo(() => {
return allForms?.map((form) => ({
value: String(form.id),
label: `${form.season} ${form.name}`,
}));
}, [allForms]);
return (
<Modal opened={opened} onClose={onClose} title={t("create contract", { capfirst: true })}>
<Title order={4}>{t("informations", { capfirst: true })}</Title>
<TextInput
label={t("contract name", { capfirst: true })}
placeholder={t("contract name", { capfirst: true })}
radius="sm"
<Modal
opened={opened}
onClose={onClose}
title={t("download contracts", { capfirst: true })}
>
<Select
label={t("form", { capfirst: true })}
placeholder={t("select a form", { capfirst: true })}
description={t(
"by selecting a form here you can download all contracts of your form",
{ capfirst: true },
)}
nothingFoundMessage={t("nothing found", { capfirst: true })}
withAsterisk
{...form.getInputProps("name")}
/>
<TextInput
label={t("contract email", { capfirst: true })}
placeholder={t("contract email", { capfirst: true })}
radius="sm"
withAsterisk
{...form.getInputProps("email")}
clearable
allowDeselect
searchable
data={formSelect || []}
{...form.getInputProps("form_id")}
/>
<Group mt="sm" justify="space-between">
<Button
@@ -61,22 +68,16 @@ export function ContractModal({
</Button>
<Button
variant="filled"
aria-label={
currentContract
? t("edit contract", { capfirst: true })
: t("create contract", { capfirst: true })
}
leftSection={currentContract ? <IconEdit /> : <IconPlus />}
aria-label={t("download contracts")}
leftSection={<IconDownload />}
onClick={() => {
form.validate();
if (form.isValid()) {
handleSubmit(form.getValues(), currentContract?.id);
handleSubmit(Number(form.values.form_id));
}
}}
>
{currentContract
? t("edit contract", { capfirst: true })
: t("create contract", { capfirst: true })}
{t("download contracts", { capfirst: true })}
</Button>
</Group>
</Modal>

View File

@@ -10,24 +10,17 @@ export type ContractRowProps = {
};
export default function ContractRow({ contract }: ContractRowProps) {
// const [searchParams] = useSearchParams();
const deleteMutation = useDeleteContract();
// const navigate = useNavigate();
const {refetch, isFetching} = useGetContractFile(contract.id)
const handleDownload = useCallback(async () => {
const { data } = await refetch();
if (!data)
return;
const getContractMutation = useGetContractFile();
const url = URL.createObjectURL(data.blob);
const link = document.createElement("a");
link.href = url;
link.download = data.filename;
link.click();
URL.revokeObjectURL(url);
}, [useGetContractFile])
const handleDownload = useCallback(async () => {
getContractMutation.mutateAsync(contract.id);
}, [useGetContractFile, contract, contract.id]);
return (
<Table.Tr key={contract.id}>
<Table.Td>
{contract.form.name} {contract.form.season}
</Table.Td>
<Table.Td>
{contract.firstname} {contract.lastname}
</Table.Td>
@@ -36,32 +29,16 @@ export default function ContractRow({ contract }: ContractRowProps) {
{contract.cheque_quantity > 0 && contract.cheque_quantity} {contract.payment_method}
</Table.Td>
<Table.Td>
{/* <Tooltip label={t("edit contract", { capfirst: true })}>
<Tooltip label={t("download contract", { capfirst: true })}>
<ActionIcon
size="sm"
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(
`/dashboard/contracts/${contract.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
);
handleDownload();
}}
>
<IconEdit />
</ActionIcon>
</Tooltip> */}
<Tooltip
label={t("download contract")}
>
<ActionIcon
size="sm"
mr="5"
onClick={(e) => {
e.stopPropagation();
handleDownload()
}}
>
<IconDownload/>
<IconDownload />
</ActionIcon>
</Tooltip>
<Tooltip label={t("remove contract", { capfirst: true })}>

View File

@@ -1,31 +1,52 @@
import { NavLink } from "react-router";
import { t } from "@/config/i18n";
import "./index.css";
import { Group } from "@mantine/core";
import { Button, Group, Loader } from "@mantine/core";
import { Config } from "@/config/config";
import { useCurrentUser, useLogoutUser } from "@/services/api";
export function Navbar() {
const { data: user, isLoading } = useCurrentUser();
const logout = useLogoutUser();
if (!user && isLoading) {
return (
<Group align="center" justify="center" h="80vh" w="100%">
<Loader color="pink" />
</Group>
);
}
return (
<nav>
<Group>
<NavLink className={"navLink"} aria-label={t("home")} to="/">
{t("home", { capfirst: true })}
</NavLink>
{user?.logged ? (
<NavLink className={"navLink"} aria-label={t("dashboard")} to="/dashboard/help">
{t("dashboard", { capfirst: true })}
</NavLink>
) : null}
</Group>
{!user?.logged ? (
<NavLink
className={"navLink"}
aria-label={t("dashboard")}
to="/dashboard/help"
aria-label={t("login with keycloak", { capfirst: true })}
to={`${Config.backend_uri}/auth/login`}
>
{t("dashboard", { capfirst: true })}
{t("login with keycloak", { capfirst: true })}
</NavLink>
</Group>
<NavLink
className={"navLink"}
aria-label={t("login with keycloak", { capfirst: true })}
to={`${Config.backend_uri}/auth/login`}
>
{t("login with keycloak", { capfirst: true })}
</NavLink>
) : (
<Button
className={"navLink"}
aria-label={t("logout", { capfirst: true })}
onClick={() => {
logout.mutate();
}}
>
{t("logout", { capfirst: true })}
</Button>
)}
</nav>
);
}

View File

@@ -3,6 +3,7 @@ import {
Group,
Modal,
MultiSelect,
Select,
TextInput,
Title,
type ModalBaseProps,
@@ -15,6 +16,8 @@ import {
type Productor,
type ProductorInputs,
} from "@/services/resources/productors";
import { useMemo } from "react";
import { useGetRoles } from "@/services/api";
export type ProductorModalProps = ModalBaseProps & {
currentProductor?: Productor;
@@ -27,6 +30,8 @@ export function ProductorModal({
currentProductor,
handleSubmit,
}: ProductorModalProps) {
const { data: allRoles } = useGetRoles();
const form = useForm<ProductorInputs>({
initialValues: {
name: currentProductor?.name ?? "",
@@ -44,6 +49,10 @@ export function ProductorModal({
},
});
const roleSelect = useMemo(() => {
return allRoles?.map((role) => ({ value: String(role.name), label: role.name }));
}, [allRoles]);
return (
<Modal opened={opened} onClose={onClose} title={t("create productor", { capfirst: true })}>
<Title order={4}>{t("Informations", { capfirst: true })}</Title>
@@ -54,11 +63,14 @@ export function ProductorModal({
withAsterisk
{...form.getInputProps("name")}
/>
<TextInput
<Select
label={t("productor type", { capfirst: true })}
placeholder={t("productor type", { capfirst: true })}
radius="sm"
withAsterisk
clearable
searchable
data={roleSelect || []}
{...form.getInputProps("type")}
/>
<TextInput

View File

@@ -19,7 +19,6 @@ import {
} from "@/services/resources/products";
import { useMemo } from "react";
import { useGetProductors } from "@/services/api";
import { InputLabel } from "@/components/Label";
export type ProductModalProps = ModalBaseProps & {
currentProduct?: Product;
@@ -90,7 +89,10 @@ export function ProductModal({ opened, onClose, currentProduct, handleSubmit }:
label={t("product type", { capfirst: true })}
placeholder={t("product type", { capfirst: true })}
radius="sm"
description={t("a product type define the way it will be organized on the final contract form (showed to users) it can be reccurent or occassional. Recurrent products will be set for all shipments if selected by user, Occasional products can be choosen for each shipments", {capfirst: true})}
description={t(
"a product type define the way it will be organized on the final contract form (showed to users) it can be reccurent or occassional. Recurrent products will be set for all shipments if selected by user, Occasional products can be choosen for each shipments",
{ capfirst: true },
)}
searchable
clearable
withAsterisk

View File

@@ -1,8 +1,19 @@
import { Button, Group, Modal, TextInput, Title, type ModalBaseProps } from "@mantine/core";
import {
Button,
Group,
Modal,
MultiSelect,
Select,
TextInput,
Title,
type ModalBaseProps,
} from "@mantine/core";
import { t } from "@/config/i18n";
import { useForm } from "@mantine/form";
import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
import { type User, type UserInputs } from "@/services/resources/users";
import { useGetRoles } from "@/services/api";
import { useMemo } from "react";
export type UserModalProps = ModalBaseProps & {
currentUser?: User;
@@ -10,10 +21,12 @@ export type UserModalProps = ModalBaseProps & {
};
export function UserModal({ opened, onClose, currentUser, handleSubmit }: UserModalProps) {
const { data: allRoles } = useGetRoles();
const form = useForm<UserInputs>({
initialValues: {
name: currentUser?.name ?? "",
email: currentUser?.email ?? "",
role_names: currentUser?.roles.map((role) => role.name) ?? [],
},
validate: {
name: (value) =>
@@ -23,6 +36,10 @@ export function UserModal({ opened, onClose, currentUser, handleSubmit }: UserMo
},
});
const roleSelect = useMemo(() => {
return allRoles?.map((role) => ({ value: String(role.name), label: role.name }));
}, [allRoles]);
return (
<Modal opened={opened} onClose={onClose} title={t("create user", { capfirst: true })}>
<Title order={4}>{t("informations", { capfirst: true })}</Title>
@@ -40,6 +57,16 @@ export function UserModal({ opened, onClose, currentUser, handleSubmit }: UserMo
withAsterisk
{...form.getInputProps("email")}
/>
<MultiSelect
label={t("user roles", { capfirst: true })}
placeholder={t("user roles", { capfirst: true })}
radius="sm"
withAsterisk
clearable
searchable
data={roleSelect || []}
{...form.getInputProps("role_names")}
/>
<Group mt="sm" justify="space-between">
<Button
variant="filled"