diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json
index d919598b..dffb0931 100644
--- a/frontend/public/locales/en/translation.json
+++ b/frontend/public/locales/en/translation.json
@@ -335,6 +335,9 @@
"ngen.password.legend2": "Your password must contain at least 8 characters.",
"ngen.password.legend3": "Your password can't be a commonly used password.",
"ngen.password.legend4": "Your password can't be entirely numeric.",
+ "password.too_short": "Your password must contain at least 8 characters.",
+ "password.entirely_numeric": "Your password can't be entirely numeric.",
+ "password.similar": "Your password can't be too similar to your other personal information.",
"ngen.phone_add": "Enter a phone number",
"ngen.phone_valid": "Enter a valid phone number",
"ngen.playbook": "Playbook",
@@ -419,9 +422,10 @@
"ngen.user": "User",
"ngen.user.ask_password": "Please enter your password",
"ngen.user.detail": "User detail",
- "ngen.user.is.staff": "Its part of the staff",
- "ngen.user.is.superuser": "Its superuser",
- "ngen.user.is.network_admin": "Its network admin",
+ "ngen.user.is.staff": "Is Staff",
+ "ngen.user.is.superuser": "Is Superuser",
+ "ngen.user.is.network_admin": "Is Network Admin",
+ "ngen.user.is.network_admin.help": "The user is related to at least one contact",
"ngen.user.placeholder": "Enter username",
"ngen.user.profile": "User Profile",
"ngen.user.username": "Username",
diff --git a/frontend/public/locales/es/translation.json b/frontend/public/locales/es/translation.json
index 9b0d947b..02f67493 100644
--- a/frontend/public/locales/es/translation.json
+++ b/frontend/public/locales/es/translation.json
@@ -335,6 +335,9 @@
"ngen.password.legend2": "Su contraseña debe contener al menos 8 caracteres.",
"ngen.password.legend3": "Su contraseña no puede ser una clave utilizada comúnmente.",
"ngen.password.legend4": "Su contraseña no puede ser completamente numérica.",
+ "password.too_short": "Su contraseña debe contener al menos 8 caracteres.",
+ "password.entirely_numeric": "Su contraseña no puede ser completamente numérica.",
+ "password.similar": "Su contraseña no puede asemejarse tanto a su otra información personal.",
"ngen.phone_add": "Ingrese un número de teléfono",
"ngen.phone_valid": "Ingrese un número de teléfono válido",
"ngen.playbook": "Playbook",
@@ -422,6 +425,7 @@
"ngen.user.is.staff": "Es parte del staff",
"ngen.user.is.superuser": "Es superusuario",
"ngen.user.is.network_admin": "Es administrador de red",
+ "ngen.user.is.network_admin.help": "El usuario se encuentra relacionado a al menos un contacto",
"ngen.user.placeholder": "Ingresar nombre de usuario",
"ngen.user.profile": "Perfil de usuario",
"ngen.user.username": "Nombre de usuario",
diff --git a/frontend/src/api/services/users.jsx b/frontend/src/api/services/users.jsx
index 16cb4887..a39b02cb 100644
--- a/frontend/src/api/services/users.jsx
+++ b/frontend/src/api/services/users.jsx
@@ -94,7 +94,7 @@ const postUser = (username, first_name, last_name, email, priority, is_active, p
});
};
-const putUser = (url, username, first_name, last_name, email, priority, is_active, groups, user_permissions, password) => {
+const putUser = (url, username, first_name, last_name, email, priority, is_active, is_superuser, is_staff, groups, user_permissions, password) => {
let messageSuccess = `El usuario ${username} se pudo editar correctamente`;
let messageError = `El usuario ${username} no se pudo editar`;
return apiInstance
@@ -105,6 +105,8 @@ const putUser = (url, username, first_name, last_name, email, priority, is_activ
email: email,
priority: priority,
is_active: is_active,
+ is_superuser: is_superuser,
+ is_staff: is_staff,
groups: groups,
user_permissions: user_permissions,
password: password
diff --git a/frontend/src/components/Field/YesNoField.jsx b/frontend/src/components/Field/YesNoField.jsx
index 8ae3632d..5c76001b 100644
--- a/frontend/src/components/Field/YesNoField.jsx
+++ b/frontend/src/components/Field/YesNoField.jsx
@@ -1,19 +1,13 @@
-import React, { useEffect, useState } from "react";
+import React from "react";
+import { Badge } from "react-bootstrap";
import { useTranslation } from "react-i18next";
const YesNoField = ({ value }) => {
const { t } = useTranslation();
-
- const [stateBool, setStateBool] = useState(null);
-
- useEffect(() => {
- setStateBool(value);
- }, [value]);
-
return (
-
- {stateBool ? t("w.yes") : t("w.no")}
-
+
+ {value ? t("w.yes") : t("w.no")}
+
);
};
diff --git a/frontend/src/utils/validators/user.jsx b/frontend/src/utils/validators/user.jsx
index 674271c7..53dce1bc 100644
--- a/frontend/src/utils/validators/user.jsx
+++ b/frontend/src/utils/validators/user.jsx
@@ -20,8 +20,46 @@ const validatePassword = (password, passwordConfirmation) => {
return !isBlank(password) && password === passwordConfirmation;
};
+const getPasswordErrors = (password, personalInfo = {}) => {
+ const errors = [];
+ if (!password) return errors;
+
+ if (password.length < 8) {
+ errors.push("password.too_short");
+ }
+ if (/^\d+$/.test(password)) {
+ errors.push("password.entirely_numeric");
+ }
+ const infoFields = [
+ personalInfo.username,
+ personalInfo.email,
+ personalInfo.first_name,
+ personalInfo.last_name,
+ ].filter(Boolean);
+ for (const field of infoFields) {
+ const parts = field.toLowerCase().split(/[@._\-\s]+/);
+ for (const part of parts) {
+ if (part.length > 2 && password.toLowerCase().includes(part)) {
+ errors.push("password.similar");
+ break;
+ }
+ }
+ if (errors.includes("password.similar")) break;
+ }
+
+ return errors;
+};
+
const validateUnrequiredInput = (input) => {
return !isBlank(input);
};
-export { validateUserName, validateName, validateUserMail, validateSelect, validatePassword, validateUnrequiredInput };
+export {
+ validateUserName,
+ validateName,
+ validateUserMail,
+ validateSelect,
+ validatePassword,
+ getPasswordErrors,
+ validateUnrequiredInput,
+};
diff --git a/frontend/src/views/contact/components/TableContact.jsx b/frontend/src/views/contact/components/TableContact.jsx
index fe5f6ac6..c03fb09e 100644
--- a/frontend/src/views/contact/components/TableContact.jsx
+++ b/frontend/src/views/contact/components/TableContact.jsx
@@ -12,6 +12,7 @@ import FormGetName from "components/Form/FormGetName";
import DateShowField from "components/Field/DateShowField";
import PermissionCheck from "components/Auth/PermissionCheck";
import PriorityComponent from "views/tanstackquery/PriorityComponent";
+import BadgeUserLabel from "views/user/components/BadgeUserLabel";
const TableContact = ({ setIsModify, list, loading, setLoading, currentPage, order, setOrder, basePath = "" }) => {
const [contact, setContact] = useState("");
@@ -160,6 +161,7 @@ const TableContact = ({ setIsModify, list, loading, setLoading, currentPage, ord
letterSize={letterSize}
/>
+
{t("ngen.user")} |
{t("ngen.action_one")} |
@@ -178,10 +180,17 @@ const TableContact = ({ setIsModify, list, loading, setLoading, currentPage, ord
-
-
- |
+
+
+ |
+
+ {contact.user ? (
+
+ ) : (
+ —
+ )}
+ |
showContact(contact.url)} />
{
user.email,
user.priority,
user.is_active,
+ user.is_superuser,
+ user.is_staff,
user.groups,
user.user_permissions,
user.password
@@ -89,6 +91,7 @@ const EditUser = () => {
createUser={editUser}
loading={loading}
passwordRequired={false}
+ isEdit={true}
/>
diff --git a/frontend/src/views/user/components/BadgeUserLabel.jsx b/frontend/src/views/user/components/BadgeUserLabel.jsx
new file mode 100644
index 00000000..6ea2f848
--- /dev/null
+++ b/frontend/src/views/user/components/BadgeUserLabel.jsx
@@ -0,0 +1,27 @@
+import React from "react";
+import { Badge } from "react-bootstrap";
+import { useQuery } from "@tanstack/react-query";
+import { getQueryUser } from "api/services/users";
+
+const BadgeUserLabel = ({ url }) => {
+ const { data, isLoading, error } = useQuery({
+ queryKey: ["userKey"],
+ queryFn: getQueryUser,
+ staleTime: 5 * 60 * 1000,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ });
+
+ if (!url || isLoading || error) return null;
+
+ const user = data?.[url];
+ if (!user?.username) return null;
+
+ return (
+
+ {user.username}
+
+ );
+};
+
+export default BadgeUserLabel;
diff --git a/frontend/src/views/user/components/FormUser.jsx b/frontend/src/views/user/components/FormUser.jsx
index f0f40845..ae572935 100644
--- a/frontend/src/views/user/components/FormUser.jsx
+++ b/frontend/src/views/user/components/FormUser.jsx
@@ -1,9 +1,10 @@
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useMemo, useState } from "react";
import { Button, Col, Form, Row, Spinner } from "react-bootstrap";
import { validateSpaces } from "../../../utils/validators";
import {
validateName,
validatePassword,
+ getPasswordErrors,
validateSelect,
validateUnrequiredInput,
validateUserMail,
@@ -15,8 +16,9 @@ import SelectComponent from "../../../components/Select/SelectComponent";
import { useTranslation } from "react-i18next";
import DualListBox from "react-dual-listbox";
import CrudButton from "components/Button/CrudButton";
+import { userIsSuperuser, userIsStaff } from "utils/permissions";
-const FormUser = ({ body, setBody, priorities, createUser, loading, passwordRequired }) => {
+const FormUser = ({ body, setBody, priorities, createUser, loading, passwordRequired, isEdit }) => {
const [selectPriority, setSelectPriority] = useState();
const [optionGroups, setOptionGroups] = useState([]);
const [optionPermissions, setOptionPermissions] = useState([]);
@@ -42,6 +44,16 @@ const FormUser = ({ body, setBody, priorities, createUser, loading, passwordRequ
});
}, []);
+ const passwordErrors = useMemo(() => {
+ if (!body.password) return [];
+ return getPasswordErrors(body.password, {
+ username: body.username || "",
+ email: body.email || "",
+ first_name: body.first_name || "",
+ last_name: body.last_name || "",
+ });
+ }, [body.password, body.username, body.email, body.first_name, body.last_name]);
+
if (loading) {
return (
@@ -72,6 +84,7 @@ const FormUser = ({ body, setBody, priorities, createUser, loading, passwordRequ
[event.target.name]: event.target.value
});
};
+
const completeField1 = (nameField, event, setOption) => {
if (event) {
setBody({
@@ -165,6 +178,53 @@ const FormUser = ({ body, setBody, priorities, createUser, loading, passwordRequ
+
+
+
+ {t("w.active")}
+
+ setBody({ ...body, is_active: e.target.checked })}
+ />
+
+
+
+ {isEdit && (
+ <>
+
+
+ {t("ngen.user.is.superuser")}
+
+ setBody({ ...body, is_superuser: e.target.checked })}
+ />
+
+
+
+
+
+ {t("ngen.user.is.staff")}
+
+ setBody({ ...body, is_staff: e.target.checked })}
+ />
+
+
+
+ >
+ )}
+
@@ -175,8 +235,16 @@ const FormUser = ({ body, setBody, priorities, createUser, loading, passwordRequ
type="password"
placeholder={passwordRequired ? t("ngen.password.placeholder") : "********"}
name="password"
+ isInvalid={body.password && passwordErrors.length > 0}
onChange={(e) => fieldPassword(e)}
/>
+ {body.password && passwordErrors.length > 0 && (
+
+ {passwordErrors.map((key) => (
+ {t(key)}
+ ))}
+
+ )}
{t("ngen.password.legend1")}
diff --git a/frontend/src/views/user/components/TableUsers.jsx b/frontend/src/views/user/components/TableUsers.jsx
index 49d8172b..733df309 100644
--- a/frontend/src/views/user/components/TableUsers.jsx
+++ b/frontend/src/views/user/components/TableUsers.jsx
@@ -12,10 +12,10 @@ import { getGroup } from "../../../api/services/groups";
import { getPermission } from "../../../api/services/permissions";
import { getPriority } from "../../../api/services/priorities";
import { useTranslation } from "react-i18next";
-import YesNoField from "components/Field/YesNoField";
import { userIsSuperuser, userIsStaff } from "utils/permissions";
import LetterFormat from "components/LetterFormat";
import UserComponent from "views/tanstackquery/UserComponent";
+import BadgeNetworkLabelContact from "views/network/components/BadgeNetworkLabelContact";
function TableUsers({ users, loading, order, setOrder, setLoading, currentPage, setIsModify }) {
@@ -158,7 +158,7 @@ function TableUsers({ users, loading, order, setOrder, setLoading, currentPage,
{t("w.active")} |
{t("ngen.user.is.superuser")} |
{t("ngen.user.is.staff")} |
- {t("ngen.user.is.network_admin")} |
+ {t("ngen.contact_other")} |
{t("session.last")} |
{t("ngen.options")} |
@@ -198,7 +198,13 @@ function TableUsers({ users, loading, order, setOrder, setLoading, currentPage,
/>
|
-
+ {user.contacts?.length > 0 ? (
+ user.contacts.map((contactUrl) => (
+
+ ))
+ ) : (
+ —
+ )}
|
{user.last_login ? user.last_login.slice(0, 10) + " " + user.last_login.slice(11, 19) : "No inicio sesion"} |
@@ -336,11 +342,17 @@ function TableUsers({ users, loading, order, setOrder, setLoading, currentPage,
) : (
<>>
)}
- {user.is_network_admin !== undefined ? (
+ {user.contacts !== undefined ? (
|
- | {t("ngen.user.is.network_admin")} |
+ {t("ngen.contact_other")} |
-
+ {user.contacts?.length > 0 ? (
+ user.contacts.map((contactUrl) => (
+
+ ))
+ ) : (
+ —
+ )}
|
) : (
diff --git a/ngen/serializers/auth.py b/ngen/serializers/auth.py
index 6f005795..f141540a 100644
--- a/ngen/serializers/auth.py
+++ b/ngen/serializers/auth.py
@@ -51,6 +51,11 @@ def create(self, validated_data):
class UserSerializer(serializers.HyperlinkedModelSerializer):
history = serializers.SerializerMethodField()
password = serializers.CharField(write_only=True, required=False)
+ contacts = serializers.HyperlinkedRelatedField(
+ many=True,
+ read_only=True,
+ view_name="contact-detail",
+ )
class Meta:
model = User
@@ -66,6 +71,7 @@ class Meta:
"password",
"is_staff",
"is_network_admin",
+ "contacts",
"is_active",
"date_joined",
"created",
diff --git a/ngen/views/auth.py b/ngen/views/auth.py
index a018dfce..bea68826 100644
--- a/ngen/views/auth.py
+++ b/ngen/views/auth.py
@@ -23,7 +23,7 @@
class UserViewSet(viewsets.ModelViewSet):
- queryset = models.User.objects.all().order_by("id")
+ queryset = models.User.objects.prefetch_related("contacts").all().order_by("id")
filter_backends = [
filters.SearchFilter,
django_filters.rest_framework.DjangoFilterBackend,