This commit is contained in:
2026-02-01 18:30:28 -06:00
parent b5ea12dd27
commit 6c02bd5448
5 changed files with 110 additions and 272 deletions

View File

@@ -3,6 +3,7 @@ import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react";
import MaterialTable from "@material-table/core"; import MaterialTable from "@material-table/core";
import { createUser, updateUser, deleteUser, getAllUsers, CreateUserInput, UpdateUserInput, User as ApiUser } from "../api/users"; import { createUser, updateUser, deleteUser, getAllUsers, CreateUserInput, UpdateUserInput, User as ApiUser } from "../api/users";
import { getAllRoles, Role as ApiRole } from "../api/roles"; import { getAllRoles, Role as ApiRole } from "../api/roles";
import { fetchProjects, type Project } from "../api/projects";
interface RoleOption { interface RoleOption {
id: string; id: string;
@@ -24,6 +25,7 @@ interface UserForm {
email: string; email: string;
password?: string; password?: string;
roleId: string; roleId: string;
projectId?: string;
status: "ACTIVE" | "INACTIVE"; status: "ACTIVE" | "INACTIVE";
createdAt: string; createdAt: string;
} }
@@ -37,12 +39,14 @@ export default function UsersPage() {
const [editingId, setEditingId] = useState<string | null>(null); const [editingId, setEditingId] = useState<string | null>(null);
const [roles, setRoles] = useState<RoleOption[]>([]); const [roles, setRoles] = useState<RoleOption[]>([]);
const [modalRoles, setModalRoles] = useState<ApiRole[]>([]); const [modalRoles, setModalRoles] = useState<ApiRole[]>([]);
const [projects, setProjects] = useState<Project[]>([]);
const [loadingUsers, setLoadingUsers] = useState(true); const [loadingUsers, setLoadingUsers] = useState(true);
const [loadingModalRoles, setLoadingModalRoles] = useState(false); const [loadingModalRoles, setLoadingModalRoles] = useState(false);
const [loadingProjects, setLoadingProjects] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const emptyUser: UserForm = { name: "", email: "", roleId: "", password: "", status: "ACTIVE", createdAt: new Date().toISOString().slice(0,10) }; const emptyUser: UserForm = { name: "", email: "", roleId: "", projectId: "", password: "", status: "ACTIVE", createdAt: new Date().toISOString().slice(0,10) };
const [form, setForm] = useState<UserForm>(emptyUser); const [form, setForm] = useState<UserForm>(emptyUser);
useEffect(() => { useEffect(() => {
@@ -96,6 +100,14 @@ export default function UsersPage() {
return; return;
} }
const selectedRole = modalRoles.find(r => r.id === form.roleId);
const isOperatorRole = selectedRole?.name === "OPERATOR";
if (isOperatorRole && !form.projectId) {
setError("Project is required for OPERATOR role");
return;
}
if (!editingId && !form.password) { if (!editingId && !form.password) {
setError("Password is required for new users"); setError("Password is required for new users");
return; return;
@@ -181,12 +193,26 @@ export default function UsersPage() {
} }
}; };
const fetchModalProjects = async () => {
try {
setLoadingProjects(true);
const projectsData = await fetchProjects();
console.log('Projects fetched:', projectsData);
setProjects(projectsData);
} catch (error) {
console.error('Failed to fetch projects:', error);
} finally {
setLoadingProjects(false);
}
};
const handleOpenAddModal = () => { const handleOpenAddModal = () => {
setForm(emptyUser); setForm(emptyUser);
setEditingId(null); setEditingId(null);
setError(null); setError(null);
setShowModal(true); setShowModal(true);
fetchModalRoles(); fetchModalRoles();
fetchModalProjects();
}; };
const handleOpenEditModal = (user: User) => { const handleOpenEditModal = (user: User) => {
@@ -195,6 +221,7 @@ export default function UsersPage() {
name: user.name, name: user.name,
email: user.email, email: user.email,
roleId: user.roleId, roleId: user.roleId,
projectId: "",
status: user.status, status: user.status,
createdAt: user.createdAt, createdAt: user.createdAt,
password: "" password: ""
@@ -202,6 +229,7 @@ export default function UsersPage() {
setError(null); setError(null);
setShowModal(true); setShowModal(true);
fetchModalRoles(); fetchModalRoles();
fetchModalProjects();
}; };
// Filter users by search and selected role // Filter users by search and selected role
@@ -327,7 +355,7 @@ export default function UsersPage() {
<select <select
value={form.roleId} value={form.roleId}
onChange={e => setForm({...form, roleId: e.target.value})} onChange={e => setForm({...form, roleId: e.target.value, projectId: ""})}
className="w-full border px-3 py-2 rounded" className="w-full border px-3 py-2 rounded"
disabled={loadingModalRoles || saving} disabled={loadingModalRoles || saving}
> >
@@ -335,6 +363,18 @@ export default function UsersPage() {
{modalRoles.map(r => <option key={r.id} value={r.id}>{r.name}</option>)} {modalRoles.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
</select> </select>
{modalRoles.find(r => r.id === form.roleId)?.name === "OPERATOR" && (
<select
value={form.projectId || ""}
onChange={e => setForm({...form, projectId: e.target.value})}
className="w-full border px-3 py-2 rounded"
disabled={loadingProjects || saving}
>
<option value="">{loadingProjects ? "Loading projects..." : "Select Project *"}</option>
{projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
)}
<button <button
onClick={() => setForm({...form, status: form.status === "ACTIVE" ? "INACTIVE" : "ACTIVE"})} onClick={() => setForm({...form, status: form.status === "ACTIVE" ? "INACTIVE" : "ACTIVE"})}
className="w-full border rounded px-3 py-2" className="w-full border rounded px-3 py-2"

View File

@@ -64,7 +64,6 @@ export default function ConcentratorsPage() {
const [errors, setErrors] = useState<Record<string, boolean>>({}); const [errors, setErrors] = useState<Record<string, boolean>>({});
const searchFiltered = useMemo(() => { const searchFiltered = useMemo(() => {
if (!c.isGeneral) return [];
return c.filteredConcentrators.filter((row) => { return c.filteredConcentrators.filter((row) => {
const q = search.trim().toLowerCase(); const q = search.trim().toLowerCase();
if (!q) return true; if (!q) return true;
@@ -72,7 +71,7 @@ export default function ConcentratorsPage() {
const sn = (row.serialNumber ?? "").toLowerCase(); const sn = (row.serialNumber ?? "").toLowerCase();
return name.includes(q) || sn.includes(q); return name.includes(q) || sn.includes(q);
}); });
}, [c.filteredConcentrators, c.isGeneral, search]); }, [c.filteredConcentrators, search]);
const validateForm = () => { const validateForm = () => {
const next: Record<string, boolean> = {}; const next: Record<string, boolean> = {};
@@ -86,7 +85,6 @@ export default function ConcentratorsPage() {
}; };
const handleSave = async () => { const handleSave = async () => {
if (!c.isGeneral) return;
if (!validateForm()) return; if (!validateForm()) return;
try { try {
@@ -110,7 +108,6 @@ export default function ConcentratorsPage() {
}; };
const handleDelete = async () => { const handleDelete = async () => {
if (!c.isGeneral) return;
if (!activeConcentrator) return; if (!activeConcentrator) return;
try { try {
@@ -124,7 +121,7 @@ export default function ConcentratorsPage() {
}; };
const openEditModal = () => { const openEditModal = () => {
if (!c.isGeneral || !activeConcentrator) return; if (!activeConcentrator) return;
setEditingId(activeConcentrator.id); setEditingId(activeConcentrator.id);
setForm({ setForm({
@@ -142,8 +139,6 @@ export default function ConcentratorsPage() {
}; };
const openCreateModal = () => { const openCreateModal = () => {
if (!c.isGeneral) return;
setForm(getEmptyForm()); setForm(getEmptyForm());
setErrors({}); setErrors({});
setEditingId(null); setEditingId(null);
@@ -174,7 +169,7 @@ export default function ConcentratorsPage() {
}} }}
projects={c.projectsData} projects={c.projectsData}
onRefresh={c.loadConcentrators} onRefresh={c.loadConcentrators}
refreshDisabled={c.loadingProjects || !c.isGeneral} refreshDisabled={c.loadingProjects}
/> />
</aside> </aside>
@@ -186,10 +181,8 @@ export default function ConcentratorsPage() {
<div> <div>
<h1 className="text-2xl font-bold">Concentrator Management</h1> <h1 className="text-2xl font-bold">Concentrator Management</h1>
<p className="text-sm text-blue-100"> <p className="text-sm text-blue-100">
{!c.isGeneral {c.selectedProject
? `Vista: ${c.sampleViewLabel} (mock)` ? `Proyecto: ${c.selectedProject} • Tipo: ${c.sampleViewLabel}`
: c.selectedProject
? `Proyecto: ${c.selectedProject}`
: "Selecciona un proyecto desde el panel izquierdo"} : "Selecciona un proyecto desde el panel izquierdo"}
</p> </p>
</div> </div>
@@ -197,7 +190,7 @@ export default function ConcentratorsPage() {
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
onClick={openCreateModal} onClick={openCreateModal}
disabled={!c.isGeneral || c.allProjects.length === 0} disabled={c.allProjects.length === 0}
className="flex items-center gap-2 px-4 py-2 bg-white text-[#4c5f9e] rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" className="flex items-center gap-2 px-4 py-2 bg-white text-[#4c5f9e] rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
> >
<Plus size={16} /> Agregar <Plus size={16} /> Agregar
@@ -205,7 +198,7 @@ export default function ConcentratorsPage() {
<button <button
onClick={openEditModal} onClick={openEditModal}
disabled={!c.isGeneral || !activeConcentrator} disabled={!activeConcentrator}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60" className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
> >
<Pencil size={16} /> Editar <Pencil size={16} /> Editar
@@ -213,7 +206,7 @@ export default function ConcentratorsPage() {
<button <button
onClick={() => setConfirmOpen(true)} onClick={() => setConfirmOpen(true)}
disabled={!c.isGeneral || !activeConcentrator} disabled={!activeConcentrator}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60" className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
> >
<Trash2 size={16} /> Eliminar <Trash2 size={16} /> Eliminar
@@ -221,7 +214,6 @@ export default function ConcentratorsPage() {
<button <button
onClick={c.loadConcentrators} onClick={c.loadConcentrators}
disabled={!c.isGeneral}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60" className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
> >
<RefreshCcw size={16} /> Actualizar <RefreshCcw size={16} /> Actualizar
@@ -231,22 +223,20 @@ export default function ConcentratorsPage() {
<input <input
className="bg-white rounded-lg shadow px-4 py-2 text-sm" className="bg-white rounded-lg shadow px-4 py-2 text-sm"
placeholder={c.isGeneral ? "Buscar concentrador..." : "Search disabled in mock views"} placeholder="Buscar concentrador..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
disabled={!c.isGeneral || !c.selectedProject} disabled={!c.selectedProject}
/> />
<div className={!c.isGeneral || !c.selectedProject ? "opacity-60 pointer-events-none" : ""}> <div className={!c.selectedProject ? "opacity-60 pointer-events-none" : ""}>
<ConcentratorsTable <ConcentratorsTable
isLoading={c.isGeneral ? c.loadingConcentrators : false} isLoading={c.loadingConcentrators}
data={c.isGeneral ? searchFiltered : []} data={searchFiltered}
activeRowId={activeConcentrator?.id} activeRowId={activeConcentrator?.id}
onRowClick={(row) => setActiveConcentrator(row)} onRowClick={(row) => setActiveConcentrator(row)}
emptyMessage={ emptyMessage={
!c.isGeneral !c.selectedProject
? `Vista "${c.sampleViewLabel}" está en modo mock (sin backend todavía).`
: !c.selectedProject
? "Selecciona un proyecto para ver los concentradores." ? "Selecciona un proyecto para ver los concentradores."
: c.loadingConcentrators : c.loadingConcentrators
? "Cargando concentradores..." ? "Cargando concentradores..."
@@ -267,7 +257,6 @@ export default function ConcentratorsPage() {
loading={deleting} loading={deleting}
onClose={() => setConfirmOpen(false)} onClose={() => setConfirmOpen(false)}
onConfirm={async () => { onConfirm={async () => {
if (!c.isGeneral) return;
setDeleting(true); setDeleting(true);
try { try {
await handleDelete(); await handleDelete();
@@ -279,7 +268,7 @@ export default function ConcentratorsPage() {
/> />
</main> </main>
{showModal && c.isGeneral && ( {showModal && (
<ConcentratorsModal <ConcentratorsModal
editingId={editingId} editingId={editingId}
form={form} form={form}

View File

@@ -126,11 +126,14 @@ export default function ConcentratorsSidebar({
{/* List */} {/* List */}
<div className="mt-4 overflow-y-auto flex-1 space-y-3 pr-1"> <div className="mt-4 overflow-y-auto flex-1 space-y-3 pr-1">
{loadingProjects && sampleView === "GENERAL" ? ( {loadingProjects ? (
<div className="text-sm text-gray-500">Loading projects...</div> <div className="text-sm text-gray-500">Loading projects...</div>
) : projects.length === 0 ? ( ) : projects.length === 0 ? (
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
No projects available. Please contact your administrator. {sampleView === "GENERAL"
? "No projects available. Please contact your administrator."
: `No projects with ${sampleViewLabel} concentrators found.`
}
</div> </div>
) : ( ) : (
projects.map((p) => { projects.map((p) => {

View File

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react";
import { import {
fetchConcentrators, fetchConcentrators,
type Concentrator, type Concentrator,
type ConcentratorType,
} from "../../api/concentrators"; } from "../../api/concentrators";
import { fetchProjects, type Project } from "../../api/projects"; import { fetchProjects, type Project } from "../../api/projects";
import type { ProjectCard, SampleView } from "./ConcentratorsPage"; import type { ProjectCard, SampleView } from "./ConcentratorsPage";
@@ -78,8 +79,6 @@ export function useConcentrators(currentUser: User) {
}; };
const loadConcentrators = async () => { const loadConcentrators = async () => {
if (!isGeneral) return;
setLoadingConcentrators(true); setLoadingConcentrators(true);
try { try {
@@ -102,14 +101,8 @@ export function useConcentrators(currentUser: User) {
// view changes // view changes
useEffect(() => { useEffect(() => {
if (isGeneral) {
loadProjects(); loadProjects();
loadConcentrators(); loadConcentrators();
} else {
setLoadingProjects(false);
setLoadingConcentrators(false);
setSelectedProject("");
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [sampleView]); }, [sampleView]);
@@ -121,25 +114,41 @@ export function useConcentrators(currentUser: User) {
} }
}, [visibleProjects, selectedProject, isGeneral]); }, [visibleProjects, selectedProject, isGeneral]);
// filter by project
useEffect(() => { useEffect(() => {
if (!isGeneral) { let filtered = concentrators;
setFilteredConcentrators([]);
return;
}
if (selectedProject) { if (selectedProject) {
setFilteredConcentrators( filtered = filtered.filter((c) => c.projectId === selectedProject);
concentrators.filter((c) => c.projectId === selectedProject)
);
} else {
setFilteredConcentrators(concentrators);
} }
}, [selectedProject, concentrators, isGeneral]);
if (!isGeneral) {
const typeMap: Record<Exclude<SampleView, "GENERAL">, ConcentratorType> = {
LORA: "LORA",
LORAWAN: "LORAWAN",
GRANDES: "GRANDES",
};
const targetType = typeMap[sampleView as Exclude<SampleView, "GENERAL">];
filtered = filtered.filter((c) => c.type === targetType);
}
setFilteredConcentrators(filtered);
}, [selectedProject, concentrators, isGeneral, sampleView]);
// sidebar cards (general) // sidebar cards (general)
const projectsDataGeneral: ProjectCard[] = useMemo(() => { const projectsDataGeneral: ProjectCard[] = useMemo(() => {
const counts = concentrators.reduce<Record<string, number>>((acc, c) => { let concentratorsToCount = concentrators;
if (!isGeneral) {
const typeMap: Record<Exclude<SampleView, "GENERAL">, ConcentratorType> = {
LORA: "LORA",
LORAWAN: "LORAWAN",
GRANDES: "GRANDES",
};
const targetType = typeMap[sampleView as Exclude<SampleView, "GENERAL">];
concentratorsToCount = concentrators.filter((c) => c.type === targetType);
}
const counts = concentratorsToCount.reduce<Record<string, number>>((acc, c) => {
const project = c.projectId ?? "SIN PROYECTO"; const project = c.projectId ?? "SIN PROYECTO";
acc[project] = (acc[project] ?? 0) + 1; acc[project] = (acc[project] ?? 0) + 1;
return acc; return acc;
@@ -165,70 +174,11 @@ export function useConcentrators(currentUser: User) {
contact: baseContact, contact: baseContact,
status: "ACTIVO" as const, status: "ACTIVO" as const,
})); }));
}, [concentrators, visibleProjects, projects]); }, [concentrators, visibleProjects, projects, isGeneral, sampleView]);
// sidebar cards (mock)
const projectsDataMock: Record<Exclude<SampleView, "GENERAL">, ProjectCard[]> =
useMemo(
() => ({
LORA: [
{
id: "mock-lora-centro",
name: "LoRa - Zona Centro",
region: "Baja California",
projects: 1,
concentrators: 12,
activeAlerts: 1,
lastSync: "Hace 15 min",
contact: "Operaciones",
status: "ACTIVO",
},
{
id: "mock-lora-este",
name: "LoRa - Zona Este",
region: "Baja California",
projects: 1,
concentrators: 8,
activeAlerts: 0,
lastSync: "Hace 40 min",
contact: "Operaciones",
status: "ACTIVO",
},
],
LORAWAN: [
{
id: "mock-lorawan-industrial",
name: "LoRaWAN - Industrial",
region: "Baja California",
projects: 1,
concentrators: 5,
activeAlerts: 0,
lastSync: "Hace 1 h",
contact: "Operaciones",
status: "ACTIVO",
},
],
GRANDES: [
{
id: "mock-grandes-convenios",
name: "Grandes - Convenios",
region: "Baja California",
projects: 1,
concentrators: 3,
activeAlerts: 0,
lastSync: "Hace 2 h",
contact: "Operaciones",
status: "ACTIVO",
},
],
}),
[]
);
const projectsData: ProjectCard[] = useMemo(() => { const projectsData: ProjectCard[] = useMemo(() => {
if (isGeneral) return projectsDataGeneral; return projectsDataGeneral;
return projectsDataMock[sampleView as Exclude<SampleView, "GENERAL">]; }, [projectsDataGeneral]);
}, [isGeneral, projectsDataGeneral, projectsDataMock, sampleView]);
return { return {
// view // view

View File

@@ -18,31 +18,6 @@ function methodToAction(method: string): AuditAction {
} }
} }
function extractSection(path: string): string {
const cleanPath = path.replace(/^\/api\//, '').replace(/^\//, '');
const segments = cleanPath.split('/').filter(s => s.length > 0);
if (segments.length === 0) return 'general';
const sectionMapping: Record<string, string> = {
'auth': 'authentication',
'me': 'profile',
'users': 'user-management',
'roles': 'role-management',
'meters': 'meters',
'concentrators': 'concentrators',
'gateways': 'gateways',
'devices': 'devices',
'projects': 'projects',
'readings': 'readings',
'bulk-upload': 'bulk-operations',
'audit-logs': 'audit',
'webhooks': 'webhooks',
};
return sectionMapping[segments[0]] || segments[0];
}
function extractTableName(path: string): string { function extractTableName(path: string): string {
const cleanPath = path.replace(/^\/api\//, '').replace(/^\//, ''); const cleanPath = path.replace(/^\/api\//, '').replace(/^\//, '');
const segments = cleanPath.split('/').filter(s => s.length > 0); const segments = cleanPath.split('/').filter(s => s.length > 0);
@@ -52,48 +27,22 @@ function extractTableName(path: string): string {
} }
const baseTableMapping: Record<string, string> = { const baseTableMapping: Record<string, string> = {
'auth': 'users',
'me': 'users',
'users': 'users',
'roles': 'roles',
'meters': 'meters', 'meters': 'meters',
'concentrators': 'concentrators', 'concentrators': 'concentrators',
'gateways': 'gateways',
'devices': 'devices',
'projects': 'projects',
'readings': 'readings',
'webhooks': 'webhooks',
'bulk-upload': 'bulk_operations',
'audit-logs': 'audit_logs',
}; };
const operations = new Set([
'password',
'stats',
'health',
'template',
'summary',
'statistics',
'my-activity',
'record',
'refresh',
'logout',
'uplink',
'join',
'ack',
'dev-eui',
]);
const nestedResourceMapping: Record<string, string> = { const nestedResourceMapping: Record<string, string> = {
'meters/readings': 'readings',
'concentrators/meters': 'meters', 'concentrators/meters': 'meters',
'gateways/devices': 'devices',
'projects/meters': 'meters', 'projects/meters': 'meters',
'projects/concentrators': 'concentrators', 'projects/concentrators': 'concentrators',
}; };
const firstSegment = segments[0]; const firstSegment = segments[0];
const baseTable = baseTableMapping[firstSegment] || firstSegment; const baseTable = baseTableMapping[firstSegment];
if (!baseTable) {
return 'unknown';
}
if (segments.length === 1) { if (segments.length === 1) {
return baseTable; return baseTable;
@@ -109,36 +58,9 @@ function extractTableName(path: string): string {
return nestedResourceMapping[nestedKey]; return nestedResourceMapping[nestedKey];
} }
if (operations.has(thirdSegment)) {
return baseTable; return baseTable;
} }
if (baseTableMapping[thirdSegment] || isPluralResourceName(thirdSegment)) {
return baseTableMapping[thirdSegment] || thirdSegment;
}
return baseTable;
}
if (firstSegment === 'bulk-upload' && secondSegment) {
return baseTableMapping[secondSegment] || secondSegment;
}
if (firstSegment === 'audit-logs') {
if (secondSegment === 'record' && segments.length >= 4) {
return segments[3];
}
return 'audit_logs';
}
if (firstSegment === 'auth') {
return 'users';
}
if (firstSegment === 'devices' && secondSegment === 'dev-eui') {
return 'devices';
}
if (segments.length === 2 && isUUID(secondSegment)) { if (segments.length === 2 && isUUID(secondSegment)) {
return baseTable; return baseTable;
} }
@@ -151,10 +73,6 @@ function isUUID(str: string): boolean {
return uuidRegex.test(str) || /^\d+$/.test(str); return uuidRegex.test(str) || /^\d+$/.test(str);
} }
function isPluralResourceName(str: string): boolean {
return str.endsWith('s') || str.endsWith('ies') || str.includes('-');
}
function extractRecordId(req: Request): string | undefined { function extractRecordId(req: Request): string | undefined {
if (req.params.id) { if (req.params.id) {
return req.params.id; return req.params.id;
@@ -166,23 +84,10 @@ function extractRecordId(req: Request): string | undefined {
return match ? match[0] : undefined; return match ? match[0] : undefined;
} }
const EXCLUDED_PATHS = [
'/api/auth/refresh',
'/api/audit-logs',
'/webhooks',
'/health',
];
function shouldExclude(path: string): boolean {
return EXCLUDED_PATHS.some(excluded => path.startsWith(excluded));
}
function generateDescription( function generateDescription(
fullPath: string,
action: AuditAction, action: AuditAction,
tableName: string, tableName: string,
recordId?: string, recordId?: string
section?: string
): string { ): string {
const actionDescriptions: Record<AuditAction, string> = { const actionDescriptions: Record<AuditAction, string> = {
'CREATE': 'Created', 'CREATE': 'Created',
@@ -198,55 +103,17 @@ function generateDescription(
}; };
const tableLabels: Record<string, string> = { const tableLabels: Record<string, string> = {
'users': 'user',
'roles': 'role',
'meters': 'meter', 'meters': 'meter',
'concentrators': 'concentrator', 'concentrators': 'concentrator',
'gateways': 'gateway',
'devices': 'device',
'projects': 'project',
'readings': 'reading',
'bulk_operations': 'bulk operation',
}; };
const actionLabel = actionDescriptions[action] || action; const actionLabel = actionDescriptions[action] || action;
const tableLabel = tableLabels[tableName] || tableName; const tableLabel = tableLabels[tableName] || tableName;
if (fullPath === '/api/me' || fullPath === '/me' || fullPath.endsWith('/auth/me')) {
return 'Viewed own profile';
}
if (fullPath.includes('/users') && action === 'READ' && !recordId) {
return 'Viewed users list';
}
if (fullPath.includes('/meters') && action === 'READ' && !recordId) {
return 'Viewed meters list';
}
if (fullPath.includes('/concentrators') && action === 'READ' && !recordId) {
return 'Viewed concentrators list';
}
if (fullPath.includes('/projects') && action === 'READ' && !recordId) {
return 'Viewed projects list';
}
if (fullPath.includes('/readings') && action === 'READ' && !recordId) {
return 'Viewed readings list';
}
if (recordId) { if (recordId) {
if (tableName === 'unknown' && section) {
return `${actionLabel} record in ${section} (ID: ${recordId.substring(0, 8)}...)`;
}
return `${actionLabel} ${tableLabel} (ID: ${recordId.substring(0, 8)}...)`; return `${actionLabel} ${tableLabel} (ID: ${recordId.substring(0, 8)}...)`;
} }
if (tableName === 'unknown' && section) {
return `${actionLabel} in ${section} section`;
}
return `${actionLabel} ${tableLabel}`; return `${actionLabel} ${tableLabel}`;
} }
@@ -256,11 +123,6 @@ export function auditMiddleware(
next: NextFunction next: NextFunction
): void { ): void {
if (shouldExclude(req.path)) {
next();
return;
}
if (!req.path.startsWith('/api')) { if (!req.path.startsWith('/api')) {
next(); next();
return; return;
@@ -286,24 +148,18 @@ export function auditMiddleware(
return; return;
} }
const action = methodToAction(req.method);
const fullPath = (req.originalUrl || req.url).split('?')[0]; const fullPath = (req.originalUrl || req.url).split('?')[0];
const section = extractSection(fullPath);
const tableName = extractTableName(fullPath); const tableName = extractTableName(fullPath);
const allowedTables = ['concentrators', 'meters'];
if (!allowedTables.includes(tableName)) {
return;
}
const action = methodToAction(req.method);
const recordId = extractRecordId(req); const recordId = extractRecordId(req);
const success = res.statusCode >= 200 && res.statusCode < 400; const success = res.statusCode >= 200 && res.statusCode < 400;
const description = generateDescription(action, tableName, recordId);
let description = generateDescription(fullPath, action, tableName, recordId, section);
if (fullPath.includes('/login')) {
description = 'User logged in successfully';
} else if (fullPath.includes('/logout')) {
description = 'User logged out';
} else if (fullPath.includes('/password')) {
description = 'Password changed';
} else if (fullPath.includes('/bulk-upload')) {
description = `Bulk upload for ${tableName}`;
}
let newValues: unknown = undefined; let newValues: unknown = undefined;
let oldValues: unknown = undefined; let oldValues: unknown = undefined;