Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions frontend/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions frontend/public/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/api/services/users.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
16 changes: 5 additions & 11 deletions frontend/src/components/Field/YesNoField.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<React.Fragment>
<p>{stateBool ? t("w.yes") : t("w.no")}</p>
</React.Fragment>
<Badge bg={value ? "success" : "secondary"} pill style={{ fontSize: "0.8rem" }}>
{value ? t("w.yes") : t("w.no")}
</Badge>
);
};

Expand Down
40 changes: 39 additions & 1 deletion frontend/src/utils/validators/user.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
15 changes: 12 additions & 3 deletions frontend/src/views/contact/components/TableContact.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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("");
Expand Down Expand Up @@ -160,6 +161,7 @@ const TableContact = ({ setIsModify, list, loading, setLoading, currentPage, ord
letterSize={letterSize}
/>
</PermissionCheck>
<th style={letterSize}>{t("ngen.user")}</th>
<th>{t("ngen.action_one")}</th>
</tr>
</thead>
Expand All @@ -178,10 +180,17 @@ const TableContact = ({ setIsModify, list, loading, setLoading, currentPage, ord
<PriorityComponent priority={contact.priority} />
</td>
<PermissionCheck permissions={["view_contactcheck","add_contactcheck"]}>
<td>
<ContactCheckButton url={contact.last_check} contact_url={contact.url} />
</td>
<td>
<ContactCheckButton url={contact.last_check} contact_url={contact.url} />
</td>
</PermissionCheck>
<td>
{contact.user ? (
<BadgeUserLabel url={contact.user} />
) : (
<span className="text-muted">—</span>
)}
</td>
<td>
<CrudButton type="read" onClick={() => showContact(contact.url)} />
<CrudButton
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/views/user/EditUser.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ const EditUser = () => {
user.email,
user.priority,
user.is_active,
user.is_superuser,
user.is_staff,
user.groups,
user.user_permissions,
user.password
Expand Down Expand Up @@ -89,6 +91,7 @@ const EditUser = () => {
createUser={editUser}
loading={loading}
passwordRequired={false}
isEdit={true}
/>
</Card.Body>
</Card>
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/views/user/components/BadgeUserLabel.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Badge pill bg="info" className="mr-1">
{user.username}
</Badge>
);
};

export default BadgeUserLabel;
72 changes: 70 additions & 2 deletions frontend/src/views/user/components/FormUser.jsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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([]);
Expand All @@ -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 (
<Row className="justify-content-md-center">
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -165,6 +178,53 @@ const FormUser = ({ body, setBody, priorities, createUser, loading, passwordRequ
</Form.Group>
</Col>
</Row>
<Row className="mb-3">
<Col sm={12} lg={4}>
<Form.Group>
<Form.Label>{t("w.active")}</Form.Label>
<div style={{ transform: "scale(1.4)", transformOrigin: "left center" }}>
<Form.Check
type="switch"
id="switch-active"
checked={body.is_active}
onChange={(e) => setBody({ ...body, is_active: e.target.checked })}
/>
</div>
</Form.Group>
</Col>
{isEdit && (
<>
<Col sm={12} lg={4}>
<Form.Group>
<Form.Label>{t("ngen.user.is.superuser")}</Form.Label>
<div style={{ transform: "scale(1.4)", transformOrigin: "left center" }}>
<Form.Check
type="switch"
id="switch-superuser"
checked={body.is_superuser || false}
disabled={!userIsSuperuser()}
onChange={(e) => setBody({ ...body, is_superuser: e.target.checked })}
/>
</div>
</Form.Group>
</Col>
<Col sm={12} lg={4}>
<Form.Group>
<Form.Label>{t("ngen.user.is.staff")}</Form.Label>
<div style={{ transform: "scale(1.4)", transformOrigin: "left center" }}>
<Form.Check
type="switch"
id="switch-staff"
checked={body.is_staff || false}
disabled={!userIsSuperuser() && !userIsStaff()}
onChange={(e) => setBody({ ...body, is_staff: e.target.checked })}
/>
</div>
</Form.Group>
</Col>
</>
)}
</Row>
<Row>
<Col sm={12} lg={6}>
<Form.Group className="mb-3" controlId="formBasicPassword">
Expand All @@ -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 && (
<div className="invalid-feedback d-block">
{passwordErrors.map((key) => (
<div key={key}>{t(key)}</div>
))}
</div>
)}
</Form.Group>
<Form.Text className="text-muted">
{t("ngen.password.legend1")}
Expand Down
24 changes: 18 additions & 6 deletions frontend/src/views/user/components/TableUsers.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down Expand Up @@ -158,7 +158,7 @@ function TableUsers({ users, loading, order, setOrder, setLoading, currentPage,
<th style={letterSize}>{t("w.active")}</th>
<th style={letterSize}>{t("ngen.user.is.superuser")}</th>
<th style={letterSize}>{t("ngen.user.is.staff")}</th>
<th style={letterSize}>{t("ngen.user.is.network_admin")}</th>
<th style={letterSize} title={t("ngen.user.is.network_admin.help")}>{t("ngen.contact_other")}</th>
<th style={letterSize}>{t("session.last")}</th>
<th style={letterSize}>{t("ngen.options")}</th>
</tr>
Expand Down Expand Up @@ -198,7 +198,13 @@ function TableUsers({ users, loading, order, setOrder, setLoading, currentPage,
/>
</td>
<td>
<YesNoField value={user.is_network_admin} />
{user.contacts?.length > 0 ? (
user.contacts.map((contactUrl) => (
<BadgeNetworkLabelContact url={contactUrl} key={contactUrl} />
))
Comment thread
mateodurante marked this conversation as resolved.
) : (
<span className="text-muted">—</span>
)}
</td>
<td>{user.last_login ? user.last_login.slice(0, 10) + " " + user.last_login.slice(11, 19) : "No inicio sesion"}</td>
<td>
Expand Down Expand Up @@ -336,11 +342,17 @@ function TableUsers({ users, loading, order, setOrder, setLoading, currentPage,
) : (
<></>
)}
{user.is_network_admin !== undefined ? (
{user.contacts !== undefined ? (
<tr>
<td> {t("ngen.user.is.network_admin")}</td>
<td> {t("ngen.contact_other")}</td>
<td>
<YesNoField value={user.is_network_admin} />
{user.contacts?.length > 0 ? (
user.contacts.map((contactUrl) => (
<BadgeNetworkLabelContact url={contactUrl} key={contactUrl} />
))
Comment thread
mateodurante marked this conversation as resolved.
) : (
<span className="text-muted">—</span>
)}
</td>
</tr>
) : (
Expand Down
Loading
Loading