Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0c0a33d
feat(audit): add audit log UI — global view + per-object modal
mateodurante Jun 15, 2026
c70ef65
fix(audit): add DjangoFilterBackend to AuditViewSet + regression tests
mateodurante Jun 15, 2026
2dffba4
fix(audit): track M2M through-model changes for Event and Case
mateodurante Jun 15, 2026
a2d9280
fix(audit): add post_delete signal for TaggedObject tag removal
mateodurante Jun 16, 2026
4f961a7
fix(audit): add m2m_changed signals for standard M2M fields + Case tests
mateodurante Jun 16, 2026
cef8539
fix(audit): ReadCase/ViewNetwork/ViewContact AuditModal received unde…
mateodurante Jun 16, 2026
4352a66
feat(audit): make audit table rows expandable on click
mateodurante Jun 16, 2026
e467c3b
feat(audit): render M2M PKs as clickable links to object view pages
mateodurante Jun 16, 2026
eef5bd4
feat(audit): log Event-Case link/unlink changes on both models
mateodurante Jun 16, 2026
1db21f0
feat(audit): improve event-case link display and add FK reference links
mateodurante Jun 16, 2026
79a5c9a
fix(audit): use actor_username and content_type_model in frontend tables
mateodurante Jun 16, 2026
cd4f27f
fix(audit): catch NoReverseMatch for TaggedObject in AuditSerializer
mateodurante Jun 16, 2026
3cca9e3
feat(audit): add filters for action, actor, type, and date range
mateodurante Jun 16, 2026
b7f8437
refactor(audit): use FilterToolbar + Collapse pattern for audit filters
mateodurante Jun 16, 2026
f026f4f
style(audit): match /events filter styles — date type=date, default s…
mateodurante Jun 16, 2026
7f90526
fix(dark-mode): use solid color for react-select menu background
mateodurante Jun 16, 2026
a8b148f
style(audit): use react-select for Action filter like /events
mateodurante Jun 16, 2026
0e2726c
fix(test): use event.pk instead of hardcoded pk=1 in case post test
mateodurante Jun 16, 2026
3f1e78e
fix: event-case unlinking, signal bugs, audit security, and frontend …
mateodurante Jun 17, 2026
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
6 changes: 6 additions & 0 deletions frontend/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@
"w.language.english": "English",
"w.language.es": "Spanish",
"w.language.spanish": "Spanish",
"w.load_more": "Load more",
"w.loading": "Loading",
"w.main": "Main",
"w.modify": "Modify",
Expand Down Expand Up @@ -649,6 +650,11 @@
"w.view": "View",
"w.link": "Link",
"w.no_data": "No data available",
"w.action": "Action",
"w.changes": "Changes",
"w.unknown": "Unknown",
"ngen.audit.title": "Audit History",
"menu.audit": "Audit Log",
"button.logout": "Logout",
"ngen.dark_mode": "Dark mode"
}
6 changes: 6 additions & 0 deletions frontend/public/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@
"w.language.english": "Inglés",
"w.language.es": "Español",
"w.language.spanish": "Español",
"w.load_more": "Cargar más",
"w.loading": "Cargando",
"w.main": "Principal",
"w.modify": "Modificar",
Expand Down Expand Up @@ -649,6 +650,11 @@
"w.view": "Ver",
"w.link": "Enlace",
"w.no_data": "Sin datos disponibles",
"w.action": "Acción",
"w.changes": "Cambios",
"w.unknown": "Desconocido",
"ngen.audit.title": "Historial de Auditoría",
"menu.audit": "Registro de Auditoría",
"button.logout": "Cerrar sesión",
"ngen.dark_mode": "Modo oscuro"
}
22 changes: 22 additions & 0 deletions frontend/src/api/services/audit.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import apiInstance from "../api";
import { COMPONENT_URL } from "../../config/constant";
import setAlert from "../../utils/setAlert";

const getAudits = (currentPage, filters, order) => {
return apiInstance
.get(`${COMPONENT_URL.audit}?page=${currentPage}&ordering=${order}&${filters}`)
.then((response) => response)
.catch((error) => {
setAlert("Failed to fetch audit log", "error");
return Promise.reject(error);
});
};

const getObjectAudits = (modelName, objectId, page = 1) => {
return apiInstance
.get(`${COMPONENT_URL.audit}?content_type__model=${modelName}&object_id=${objectId}&ordering=-timestamp&page=${page}`)
.then((response) => response)
.catch(() => ({ data: { results: [], next: null } }));
};

export { getAudits, getObjectAudits };
6 changes: 0 additions & 6 deletions frontend/src/api/services/users.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,7 @@ const isActive = (url, active) => {
})
.catch((error) => {
if (error.message === "Cannot read properties of undefined (reading 'code')") {
//el backend o servidor no funciona
messageError = !active ? `El usuario no pudo ser desactivado no pudo ser` : `El usuario no pudo ser activado no pudo ser`;
setAlert(messageError, "error");
}
setAlert(messageError, "error");
return Promise.reject(error);
Expand All @@ -165,9 +163,7 @@ const isSuperuser = (url, superuser) => {
})
.catch((error) => {
if (error.message === "Cannot read properties of undefined (reading 'code')") {
//el backend o servidor no funciona
messageError = !superuser ? `El usuario no pudo ser desactivado no pudo ser` : `El usuario no pudo ser activado no pudo ser`;
setAlert(messageError, "error");
}
setAlert(messageError, "error");
return Promise.reject(error);
Expand All @@ -187,9 +183,7 @@ const isStaff = (url, staff) => {
})
.catch((error) => {
if (error.message === "Cannot read properties of undefined (reading 'code')") {
//el backend o servidor no funciona
messageError = !staff ? `El usuario no pudo ser desactivado no pudo ser` : `El usuario no pudo ser activado no pudo ser`;
setAlert(messageError, "error");
}
setAlert(messageError, "error");
return Promise.reject(error);
Expand Down
20 changes: 7 additions & 13 deletions frontend/src/api/setupInterceptors.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,26 +78,20 @@ const setup = (store) => {
});
});
} else {
// check if avoidRaise is true
if (originalRequest.avoidRaise) {
return Promise.reject(error);
}
console.log(error);
// check if data is undefined, array or object
let data = error.response?.data;
if (data === undefined) {
setAlert("Error al realizar la petición", "error");
console.error("API request failed: no response data", error.message);
} else if (Array.isArray(data)) {
let msg = "";
data.map((d) => {
msg += d + " ";
});
setAlert("Errores al realizar la petición: " + msg, "error");
console.error("API request failed:", data.join(" "));
} else {
let msg = error.response?.data?.non_field_errors ? error.response.data.non_field_errors : "";
msg = msg ? msg : error.response?.data?.detail ? error.response.data.detail : "";
msg = msg ? msg : error.response?.data?.__all__ ? error.response.data.error : "";
setAlert("Error al realizar la petición: " + msg, "error");
let msg = error.response?.data?.non_field_errors
|| error.response?.data?.detail
|| error.response?.data?.__all__
|| "";
console.error("API request failed:", msg || error.message);
}
return Promise.reject(error);
}
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/assets/scss/partials/_dark-mode.scss
Original file line number Diff line number Diff line change
Expand Up @@ -458,13 +458,12 @@
}

.react-select__menu {
background-color: var(--bs-dropdown-bg);
background-color: #1e1f22;
border: 1px solid var(--bs-border-color);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}

.react-select__menu-list {
background-color: var(--bs-dropdown-bg);
padding: 4px 0;
}

Expand Down
1 change: 1 addition & 0 deletions frontend/src/config/constant.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export const COMPONENT_URL = {
analyzer: "analyzer/",
analyzerMapping: "analyzermapping/",
version: "version/",
audit: "audit/",
lookup: "lookup/",
addressinfo: "addressinfo/",
emailmessage: "emailmessage/",
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/menu-items.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,15 @@ const menuItems = {
icon: "",
breadcrumbs: true
},
{
id: "audits",
title: "menu.audit",
type: "item",
url: "/audits",
classes: "",
icon: "",
breadcrumbs: true
},
{
id: "about",
title: "menu.about",
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,14 @@ const routes = [
permissions: ["view_lookup"],
element: lazy(() => import("./views/tools/lookup/ShowLookup"))
},
{
exact: "true",
path: "/audits",
layout: AdminLayout,
guard: PermissionGuard,
permissions: ["view_logentry"],
element: lazy(() => import("./views/audits/ListAudit"))
},
{
exact: "true",
path: "/setting",
Expand Down
194 changes: 194 additions & 0 deletions frontend/src/views/audits/ListAudit.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import React, { useEffect, useState } from "react";
import { Card, Col, Collapse, Form, Row } from "react-bootstrap";
import Select from "react-select";
import { getAudits } from "../../api/services/audit";
import AdvancedPagination from "../../components/Pagination/AdvancedPagination";
import TableAudit from "./components/TableAudit";
import Search from "../../components/Search/Search";
import FilterToolbar from "../../components/Button/FilterToolbar";
import { useTranslation } from "react-i18next";

const ACTION_OPTIONS = [
{ value: "", label: "All" },
{ value: "0", label: "Create" },
{ value: "1", label: "Update" },
{ value: "2", label: "Delete" },
];

const ListAudit = () => {
const [audits, setAudits] = useState([]);
const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [countItems, setCountItems] = useState(0);
const [order, setOrder] = useState("-timestamp");
const [wordToSearch, setWordToSearch] = useState("");
const [actionFilter, setActionFilter] = useState("");
const [actorFilter, setActorFilter] = useState("");
const [typeFilter, setTypeFilter] = useState("");
const [dateFrom, setDateFrom] = useState("");
const [dateTo, setDateTo] = useState("");
const [updatePagination, setUpdatePagination] = useState(false);
const [disabledPagination, setDisabledPagination] = useState(true);
const [open, setOpen] = useState(false);
const { t } = useTranslation();

function updatePage(chosenPage) {
setCurrentPage(chosenPage);
}

const buildFilters = () => {
const parts = [];
if (wordToSearch) parts.push(`search=${encodeURIComponent(wordToSearch)}`);
if (actionFilter) parts.push(`action=${actionFilter}`);
if (actorFilter) parts.push(`actor__username=${encodeURIComponent(actorFilter)}`);
if (typeFilter) parts.push(`content_type__model=${encodeURIComponent(typeFilter)}`);
if (dateFrom) parts.push(`timestamp_after=${encodeURIComponent(dateFrom)}`);
if (dateTo) parts.push(`timestamp_before=${encodeURIComponent(dateTo)}`);
return parts.join("&");
};

useEffect(() => {
setLoading(true);
getAudits(currentPage, buildFilters(), order)
.then((response) => {
setAudits(response.data.results);
setCountItems(response.data.count);
if (currentPage === 1) setUpdatePagination(true);
setDisabledPagination(false);
})
.catch(() => {})
.finally(() => setLoading(false));
}, [currentPage, wordToSearch, actionFilter, actorFilter, typeFilter, dateFrom, dateTo, order]);

const clearFilters = () => {
setActionFilter("");
setActorFilter("");
setTypeFilter("");
setDateFrom("");
setDateTo("");
setCurrentPage(1);
};

return (
<React.Fragment>
<Row>
<Col>
<Card>
<Card.Header>
<Row>
<Col sm="auto">
<FilterToolbar open={open} setOpen={setOpen} onReload={() => setCurrentPage(1)} onClearFilters={clearFilters} />
</Col>
<Col sm={8} lg={4}>
<Search
type={t("search.by.name.description")}
setWordToSearch={setWordToSearch}
wordToSearch={wordToSearch}
setLoading={setLoading}
setCurrentPage={setCurrentPage}
/>
</Col>
</Row>
<Collapse in={open}>
<div id="example-collapse-text">
<Row>
<Col sm={12} lg={6}>
<Form.Group controlId="formGridAddress1">
<Form.Label>{t("date.condition_from")}</Form.Label>
<Form.Control
type="date"
maxLength="150"
placeholder={t("date.condition_from")}
value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setCurrentPage(1); }}
name="date"
/>
</Form.Group>
</Col>
<Col sm={12} lg={6}>
<Form.Group controlId="formGridAddress1">
<Form.Label>{t("date.condition_to")}</Form.Label>
<Form.Control
type="date"
maxLength="150"
value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setCurrentPage(1); }}
name="date"
/>
</Form.Group>
</Col>
</Row>
<Row>
<Col sm={4} lg={4}>
<Form.Group controlId="formGridAddress1">
<Form.Label>{t("w.action")}</Form.Label>
<Select
classNamePrefix="react-select"
options={ACTION_OPTIONS.slice(1)}
isClearable
placeholder={`${t("ngen.filter_by")} ${t("w.action")}`}
value={ACTION_OPTIONS.find((o) => o.value === actionFilter) || null}
onChange={(e) => { setActionFilter(e?.value || ""); setCurrentPage(1); }}
/>
</Form.Group>
</Col>
<Col sm={4} lg={4}>
<Form.Group controlId="formGridAddress1">
<Form.Label>{t("reporter")}</Form.Label>
<Form.Control
type="text"
maxLength="150"
value={actorFilter}
placeholder={t("reporter")}
onChange={(e) => { setActorFilter(e.target.value); setCurrentPage(1); }}
name="reporter"
/>
</Form.Group>
</Col>
<Col sm={4} lg={4}>
<Form.Group controlId="formGridAddress1">
<Form.Label>{t("ngen.type")}</Form.Label>
<Form.Control
type="text"
maxLength="150"
value={typeFilter}
placeholder={t("ngen.type")}
onChange={(e) => { setTypeFilter(e.target.value); setCurrentPage(1); }}
name="type"
/>
</Form.Group>
</Col>
</Row>
</div>
</Collapse>
</Card.Header>
<TableAudit
audits={audits}
loading={loading}
order={order}
setOrder={setOrder}
setLoading={setLoading}
/>
<Card.Footer>
<Row className="justify-content-md-center">
<Col md="auto">
<AdvancedPagination
countItems={countItems}
updatePage={updatePage}
updatePagination={updatePagination}
setUpdatePagination={setUpdatePagination}
setLoading={setLoading}
setDisabledPagination={setDisabledPagination}
disabledPagination={disabledPagination}
/>
</Col>
</Row>
</Card.Footer>
</Card>
</Col>
</Row>
</React.Fragment>
);
};

export default ListAudit;
Loading
Loading