From 988bb22f0c58ecb09697e69b22140b4a726b1c47 Mon Sep 17 00:00:00 2001 From: Ngen Developer Date: Sun, 24 May 2026 03:11:48 +0200 Subject: [PATCH 1/7] improve user edit and list --- frontend/public/locales/en/translation.json | 10 ++- frontend/public/locales/es/translation.json | 4 ++ frontend/src/api/services/users.jsx | 4 +- frontend/src/components/Field/YesNoField.jsx | 16 ++--- frontend/src/utils/validators/user.jsx | 40 +++++++++++- .../views/contact/components/TableContact.jsx | 15 ++++- frontend/src/views/user/EditUser.jsx | 2 + .../views/user/components/BadgeUserLabel.jsx | 26 ++++++++ .../src/views/user/components/FormUser.jsx | 63 ++++++++++++++++++- .../src/views/user/components/TableUsers.jsx | 24 +++++-- ngen/serializers/auth.py | 6 ++ 11 files changed, 184 insertions(+), 26 deletions(-) create mode 100644 frontend/src/views/user/components/BadgeUserLabel.jsx 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 diff --git a/frontend/src/views/user/components/BadgeUserLabel.jsx b/frontend/src/views/user/components/BadgeUserLabel.jsx new file mode 100644 index 00000000..3bb2ccca --- /dev/null +++ b/frontend/src/views/user/components/BadgeUserLabel.jsx @@ -0,0 +1,26 @@ +import React, { useEffect, useState } from "react"; +import { Badge } from "react-bootstrap"; +import apiInstance from "../../../api/api"; + +const BadgeUserLabel = ({ url }) => { + const [username, setUsername] = useState(""); + + useEffect(() => { + apiInstance + .get(url) + .then((response) => { + setUsername(response.data.username); + }) + .catch(() => {}); + }, [url]); + + if (!username) return null; + + return ( + + {username} + + ); +}; + +export default BadgeUserLabel; diff --git a/frontend/src/views/user/components/FormUser.jsx b/frontend/src/views/user/components/FormUser.jsx index f0f40845..d1ef9af2 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, @@ -42,6 +43,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 +83,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 +177,47 @@ const FormUser = ({ body, setBody, priorities, createUser, loading, passwordRequ + + + + {t("w.active")} +
+ setBody({ ...body, is_active: e.target.checked })} + /> +
+
+ + + + {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 +228,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..c30672dd 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, index) => ( + + )) + ) : ( + + )} {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, index) => ( + + )) + ) : ( + + )} ) : ( 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", From f896d98752f96f8bbfd71d4b0dd8971831dc698a Mon Sep 17 00:00:00 2001 From: Mateo Durante Date: Tue, 26 May 2026 18:23:59 +0200 Subject: [PATCH 2/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- frontend/src/views/user/components/TableUsers.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/user/components/TableUsers.jsx b/frontend/src/views/user/components/TableUsers.jsx index c30672dd..0fe56707 100644 --- a/frontend/src/views/user/components/TableUsers.jsx +++ b/frontend/src/views/user/components/TableUsers.jsx @@ -199,12 +199,15 @@ function TableUsers({ users, loading, order, setOrder, setLoading, currentPage, {user.contacts?.length > 0 ? ( - user.contacts.map((contactUrl, index) => ( - + user.contacts.map((contactUrl) => ( + )) ) : ( )} + ) : ( + + )} {user.last_login ? user.last_login.slice(0, 10) + " " + user.last_login.slice(11, 19) : "No inicio sesion"} From e0f2e23b694bea3d66bf5482bac7e49253b7114d Mon Sep 17 00:00:00 2001 From: Mateo Durante Date: Tue, 26 May 2026 18:24:51 +0200 Subject: [PATCH 3/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../views/user/components/BadgeUserLabel.jsx | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/frontend/src/views/user/components/BadgeUserLabel.jsx b/frontend/src/views/user/components/BadgeUserLabel.jsx index 3bb2ccca..f89af3cb 100644 --- a/frontend/src/views/user/components/BadgeUserLabel.jsx +++ b/frontend/src/views/user/components/BadgeUserLabel.jsx @@ -1,18 +1,30 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import { Badge } from "react-bootstrap"; -import apiInstance from "../../../api/api"; +import { useQuery } from "@tanstack/react-query"; +import { getQueryUser } from "api/services/users"; const BadgeUserLabel = ({ url }) => { - const [username, setUsername] = useState(""); - - useEffect(() => { - apiInstance - .get(url) - .then((response) => { - setUsername(response.data.username); - }) - .catch(() => {}); - }, [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; if (!username) return null; From 7082ff46e66ec9fe3bfa293df4fac13b6a7e7bec Mon Sep 17 00:00:00 2001 From: Mateo Durante Date: Tue, 26 May 2026 18:25:11 +0200 Subject: [PATCH 4/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- frontend/src/views/user/components/TableUsers.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/user/components/TableUsers.jsx b/frontend/src/views/user/components/TableUsers.jsx index 0fe56707..6fa7c3a6 100644 --- a/frontend/src/views/user/components/TableUsers.jsx +++ b/frontend/src/views/user/components/TableUsers.jsx @@ -350,12 +350,15 @@ function TableUsers({ users, loading, order, setOrder, setLoading, currentPage, {t("ngen.contact_other")} {user.contacts?.length > 0 ? ( - user.contacts.map((contactUrl, index) => ( - + user.contacts.map((contactUrl) => ( + )) ) : ( )} + ) : ( + + )} ) : ( From 0b3e01977637c5bcda10eaa310eed5f7995efcb1 Mon Sep 17 00:00:00 2001 From: Ngen Developer Date: Tue, 26 May 2026 18:31:48 +0200 Subject: [PATCH 5/7] fix: add prefetch_related, fix React keys, guard superuser/staff switches --- frontend/src/views/user/EditUser.jsx | 2 +- .../src/views/user/components/FormUser.jsx | 61 +++++++++++-------- ngen/views/auth.py | 2 +- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/frontend/src/views/user/EditUser.jsx b/frontend/src/views/user/EditUser.jsx index a8ec5c3b..58da4a3e 100644 --- a/frontend/src/views/user/EditUser.jsx +++ b/frontend/src/views/user/EditUser.jsx @@ -91,8 +91,8 @@ const EditUser = () => { createUser={editUser} loading={loading} passwordRequired={false} + isEdit={true} /> - ); diff --git a/frontend/src/views/user/components/FormUser.jsx b/frontend/src/views/user/components/FormUser.jsx index d1ef9af2..ae572935 100644 --- a/frontend/src/views/user/components/FormUser.jsx +++ b/frontend/src/views/user/components/FormUser.jsx @@ -16,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([]); @@ -191,32 +192,38 @@ const FormUser = ({ body, setBody, priorities, createUser, loading, passwordRequ - - - {t("ngen.user.is.superuser")} -
- setBody({ ...body, is_superuser: e.target.checked })} - /> -
-
- - - - {t("ngen.user.is.staff")} -
- setBody({ ...body, is_staff: 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 })} + /> +
+
+ + + )}
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, From bb53424081dca7ae99856b1d92f61a4173bb6546 Mon Sep 17 00:00:00 2001 From: Ngen Developer Date: Tue, 26 May 2026 18:58:20 +0200 Subject: [PATCH 6/7] fix: add missing Card.Body closing tag --- frontend/src/views/user/EditUser.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/views/user/EditUser.jsx b/frontend/src/views/user/EditUser.jsx index 58da4a3e..07700099 100644 --- a/frontend/src/views/user/EditUser.jsx +++ b/frontend/src/views/user/EditUser.jsx @@ -93,6 +93,7 @@ const EditUser = () => { passwordRequired={false} isEdit={true} /> + ); From 262f5c6015b64c95cf67bd9ce52cff5703f1016e Mon Sep 17 00:00:00 2001 From: Ngen Developer Date: Tue, 26 May 2026 19:02:21 +0200 Subject: [PATCH 7/7] fix: resolve merge conflicts in TableUsers and BadgeUserLabel --- frontend/src/views/user/components/BadgeUserLabel.jsx | 11 ----------- frontend/src/views/user/components/TableUsers.jsx | 6 ------ 2 files changed, 17 deletions(-) diff --git a/frontend/src/views/user/components/BadgeUserLabel.jsx b/frontend/src/views/user/components/BadgeUserLabel.jsx index f89af3cb..6ea2f848 100644 --- a/frontend/src/views/user/components/BadgeUserLabel.jsx +++ b/frontend/src/views/user/components/BadgeUserLabel.jsx @@ -24,15 +24,4 @@ const BadgeUserLabel = ({ url }) => { ); }; -export default BadgeUserLabel; - - if (!username) return null; - - return ( - - {username} - - ); -}; - export default BadgeUserLabel; diff --git a/frontend/src/views/user/components/TableUsers.jsx b/frontend/src/views/user/components/TableUsers.jsx index 6fa7c3a6..733df309 100644 --- a/frontend/src/views/user/components/TableUsers.jsx +++ b/frontend/src/views/user/components/TableUsers.jsx @@ -205,9 +205,6 @@ function TableUsers({ users, loading, order, setOrder, setLoading, currentPage, ) : ( )} - ) : ( - - )} {user.last_login ? user.last_login.slice(0, 10) + " " + user.last_login.slice(11, 19) : "No inicio sesion"} @@ -356,9 +353,6 @@ function TableUsers({ users, loading, order, setOrder, setLoading, currentPage, ) : ( )} - ) : ( - - )} ) : (