Changes
This commit is contained in:
@@ -3,6 +3,7 @@ import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react";
|
||||
import MaterialTable from "@material-table/core";
|
||||
import { createUser, updateUser, deleteUser, getAllUsers, CreateUserInput, UpdateUserInput, User as ApiUser } from "../api/users";
|
||||
import { getAllRoles, Role as ApiRole } from "../api/roles";
|
||||
import { fetchProjects, type Project } from "../api/projects";
|
||||
|
||||
interface RoleOption {
|
||||
id: string;
|
||||
@@ -24,6 +25,7 @@ interface UserForm {
|
||||
email: string;
|
||||
password?: string;
|
||||
roleId: string;
|
||||
projectId?: string;
|
||||
status: "ACTIVE" | "INACTIVE";
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -37,12 +39,14 @@ export default function UsersPage() {
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [roles, setRoles] = useState<RoleOption[]>([]);
|
||||
const [modalRoles, setModalRoles] = useState<ApiRole[]>([]);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [loadingUsers, setLoadingUsers] = useState(true);
|
||||
const [loadingModalRoles, setLoadingModalRoles] = useState(false);
|
||||
const [loadingProjects, setLoadingProjects] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -96,6 +100,14 @@ export default function UsersPage() {
|
||||
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) {
|
||||
setError("Password is required for new users");
|
||||
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 = () => {
|
||||
setForm(emptyUser);
|
||||
setEditingId(null);
|
||||
setError(null);
|
||||
setShowModal(true);
|
||||
fetchModalRoles();
|
||||
fetchModalProjects();
|
||||
};
|
||||
|
||||
const handleOpenEditModal = (user: User) => {
|
||||
@@ -195,6 +221,7 @@ export default function UsersPage() {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
roleId: user.roleId,
|
||||
projectId: "",
|
||||
status: user.status,
|
||||
createdAt: user.createdAt,
|
||||
password: ""
|
||||
@@ -202,6 +229,7 @@ export default function UsersPage() {
|
||||
setError(null);
|
||||
setShowModal(true);
|
||||
fetchModalRoles();
|
||||
fetchModalProjects();
|
||||
};
|
||||
|
||||
// Filter users by search and selected role
|
||||
@@ -327,7 +355,7 @@ export default function UsersPage() {
|
||||
|
||||
<select
|
||||
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"
|
||||
disabled={loadingModalRoles || saving}
|
||||
>
|
||||
@@ -335,6 +363,18 @@ export default function UsersPage() {
|
||||
{modalRoles.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
|
||||
</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
|
||||
onClick={() => setForm({...form, status: form.status === "ACTIVE" ? "INACTIVE" : "ACTIVE"})}
|
||||
className="w-full border rounded px-3 py-2"
|
||||
|
||||
@@ -64,7 +64,6 @@ export default function ConcentratorsPage() {
|
||||
const [errors, setErrors] = useState<Record<string, boolean>>({});
|
||||
|
||||
const searchFiltered = useMemo(() => {
|
||||
if (!c.isGeneral) return [];
|
||||
return c.filteredConcentrators.filter((row) => {
|
||||
const q = search.trim().toLowerCase();
|
||||
if (!q) return true;
|
||||
@@ -72,7 +71,7 @@ export default function ConcentratorsPage() {
|
||||
const sn = (row.serialNumber ?? "").toLowerCase();
|
||||
return name.includes(q) || sn.includes(q);
|
||||
});
|
||||
}, [c.filteredConcentrators, c.isGeneral, search]);
|
||||
}, [c.filteredConcentrators, search]);
|
||||
|
||||
const validateForm = () => {
|
||||
const next: Record<string, boolean> = {};
|
||||
@@ -86,7 +85,6 @@ export default function ConcentratorsPage() {
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!c.isGeneral) return;
|
||||
if (!validateForm()) return;
|
||||
|
||||
try {
|
||||
@@ -110,7 +108,6 @@ export default function ConcentratorsPage() {
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!c.isGeneral) return;
|
||||
if (!activeConcentrator) return;
|
||||
|
||||
try {
|
||||
@@ -124,7 +121,7 @@ export default function ConcentratorsPage() {
|
||||
};
|
||||
|
||||
const openEditModal = () => {
|
||||
if (!c.isGeneral || !activeConcentrator) return;
|
||||
if (!activeConcentrator) return;
|
||||
|
||||
setEditingId(activeConcentrator.id);
|
||||
setForm({
|
||||
@@ -142,8 +139,6 @@ export default function ConcentratorsPage() {
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
if (!c.isGeneral) return;
|
||||
|
||||
setForm(getEmptyForm());
|
||||
setErrors({});
|
||||
setEditingId(null);
|
||||
@@ -174,7 +169,7 @@ export default function ConcentratorsPage() {
|
||||
}}
|
||||
projects={c.projectsData}
|
||||
onRefresh={c.loadConcentrators}
|
||||
refreshDisabled={c.loadingProjects || !c.isGeneral}
|
||||
refreshDisabled={c.loadingProjects}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
@@ -186,10 +181,8 @@ export default function ConcentratorsPage() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Concentrator Management</h1>
|
||||
<p className="text-sm text-blue-100">
|
||||
{!c.isGeneral
|
||||
? `Vista: ${c.sampleViewLabel} (mock)`
|
||||
: c.selectedProject
|
||||
? `Proyecto: ${c.selectedProject}`
|
||||
{c.selectedProject
|
||||
? `Proyecto: ${c.selectedProject} • Tipo: ${c.sampleViewLabel}`
|
||||
: "Selecciona un proyecto desde el panel izquierdo"}
|
||||
</p>
|
||||
</div>
|
||||
@@ -197,7 +190,7 @@ export default function ConcentratorsPage() {
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Plus size={16} /> Agregar
|
||||
@@ -205,7 +198,7 @@ export default function ConcentratorsPage() {
|
||||
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Pencil size={16} /> Editar
|
||||
@@ -213,7 +206,7 @@ export default function ConcentratorsPage() {
|
||||
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Trash2 size={16} /> Eliminar
|
||||
@@ -221,7 +214,6 @@ export default function ConcentratorsPage() {
|
||||
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<RefreshCcw size={16} /> Actualizar
|
||||
@@ -231,22 +223,20 @@ export default function ConcentratorsPage() {
|
||||
|
||||
<input
|
||||
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}
|
||||
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
|
||||
isLoading={c.isGeneral ? c.loadingConcentrators : false}
|
||||
data={c.isGeneral ? searchFiltered : []}
|
||||
isLoading={c.loadingConcentrators}
|
||||
data={searchFiltered}
|
||||
activeRowId={activeConcentrator?.id}
|
||||
onRowClick={(row) => setActiveConcentrator(row)}
|
||||
emptyMessage={
|
||||
!c.isGeneral
|
||||
? `Vista "${c.sampleViewLabel}" está en modo mock (sin backend todavía).`
|
||||
: !c.selectedProject
|
||||
!c.selectedProject
|
||||
? "Selecciona un proyecto para ver los concentradores."
|
||||
: c.loadingConcentrators
|
||||
? "Cargando concentradores..."
|
||||
@@ -267,7 +257,6 @@ export default function ConcentratorsPage() {
|
||||
loading={deleting}
|
||||
onClose={() => setConfirmOpen(false)}
|
||||
onConfirm={async () => {
|
||||
if (!c.isGeneral) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
await handleDelete();
|
||||
@@ -279,7 +268,7 @@ export default function ConcentratorsPage() {
|
||||
/>
|
||||
</main>
|
||||
|
||||
{showModal && c.isGeneral && (
|
||||
{showModal && (
|
||||
<ConcentratorsModal
|
||||
editingId={editingId}
|
||||
form={form}
|
||||
|
||||
@@ -126,11 +126,14 @@ export default function ConcentratorsSidebar({
|
||||
|
||||
{/* List */}
|
||||
<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>
|
||||
) : projects.length === 0 ? (
|
||||
<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>
|
||||
) : (
|
||||
projects.map((p) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
fetchConcentrators,
|
||||
type Concentrator,
|
||||
type ConcentratorType,
|
||||
} from "../../api/concentrators";
|
||||
import { fetchProjects, type Project } from "../../api/projects";
|
||||
import type { ProjectCard, SampleView } from "./ConcentratorsPage";
|
||||
@@ -78,8 +79,6 @@ export function useConcentrators(currentUser: User) {
|
||||
};
|
||||
|
||||
const loadConcentrators = async () => {
|
||||
if (!isGeneral) return;
|
||||
|
||||
setLoadingConcentrators(true);
|
||||
|
||||
try {
|
||||
@@ -102,14 +101,8 @@ export function useConcentrators(currentUser: User) {
|
||||
|
||||
// view changes
|
||||
useEffect(() => {
|
||||
if (isGeneral) {
|
||||
loadProjects();
|
||||
loadConcentrators();
|
||||
} else {
|
||||
setLoadingProjects(false);
|
||||
setLoadingConcentrators(false);
|
||||
setSelectedProject("");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sampleView]);
|
||||
|
||||
@@ -121,25 +114,41 @@ export function useConcentrators(currentUser: User) {
|
||||
}
|
||||
}, [visibleProjects, selectedProject, isGeneral]);
|
||||
|
||||
// filter by project
|
||||
useEffect(() => {
|
||||
if (!isGeneral) {
|
||||
setFilteredConcentrators([]);
|
||||
return;
|
||||
}
|
||||
let filtered = concentrators;
|
||||
|
||||
if (selectedProject) {
|
||||
setFilteredConcentrators(
|
||||
concentrators.filter((c) => c.projectId === selectedProject)
|
||||
);
|
||||
} else {
|
||||
setFilteredConcentrators(concentrators);
|
||||
filtered = filtered.filter((c) => c.projectId === selectedProject);
|
||||
}
|
||||
}, [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)
|
||||
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";
|
||||
acc[project] = (acc[project] ?? 0) + 1;
|
||||
return acc;
|
||||
@@ -165,70 +174,11 @@ export function useConcentrators(currentUser: User) {
|
||||
contact: baseContact,
|
||||
status: "ACTIVO" as const,
|
||||
}));
|
||||
}, [concentrators, visibleProjects, projects]);
|
||||
|
||||
// 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",
|
||||
},
|
||||
],
|
||||
}),
|
||||
[]
|
||||
);
|
||||
}, [concentrators, visibleProjects, projects, isGeneral, sampleView]);
|
||||
|
||||
const projectsData: ProjectCard[] = useMemo(() => {
|
||||
if (isGeneral) return projectsDataGeneral;
|
||||
return projectsDataMock[sampleView as Exclude<SampleView, "GENERAL">];
|
||||
}, [isGeneral, projectsDataGeneral, projectsDataMock, sampleView]);
|
||||
return projectsDataGeneral;
|
||||
}, [projectsDataGeneral]);
|
||||
|
||||
return {
|
||||
// view
|
||||
|
||||
@@ -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 {
|
||||
const cleanPath = path.replace(/^\/api\//, '').replace(/^\//, '');
|
||||
const segments = cleanPath.split('/').filter(s => s.length > 0);
|
||||
@@ -52,48 +27,22 @@ function extractTableName(path: string): string {
|
||||
}
|
||||
|
||||
const baseTableMapping: Record<string, string> = {
|
||||
'auth': 'users',
|
||||
'me': 'users',
|
||||
'users': 'users',
|
||||
'roles': 'roles',
|
||||
'meters': 'meters',
|
||||
'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> = {
|
||||
'meters/readings': 'readings',
|
||||
'concentrators/meters': 'meters',
|
||||
'gateways/devices': 'devices',
|
||||
'projects/meters': 'meters',
|
||||
'projects/concentrators': 'concentrators',
|
||||
};
|
||||
|
||||
const firstSegment = segments[0];
|
||||
const baseTable = baseTableMapping[firstSegment] || firstSegment;
|
||||
const baseTable = baseTableMapping[firstSegment];
|
||||
|
||||
if (!baseTable) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
if (segments.length === 1) {
|
||||
return baseTable;
|
||||
@@ -109,36 +58,9 @@ function extractTableName(path: string): string {
|
||||
return nestedResourceMapping[nestedKey];
|
||||
}
|
||||
|
||||
if (operations.has(thirdSegment)) {
|
||||
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)) {
|
||||
return baseTable;
|
||||
}
|
||||
@@ -151,10 +73,6 @@ function isUUID(str: string): boolean {
|
||||
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 {
|
||||
if (req.params.id) {
|
||||
return req.params.id;
|
||||
@@ -166,23 +84,10 @@ function extractRecordId(req: Request): string | 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(
|
||||
fullPath: string,
|
||||
action: AuditAction,
|
||||
tableName: string,
|
||||
recordId?: string,
|
||||
section?: string
|
||||
recordId?: string
|
||||
): string {
|
||||
const actionDescriptions: Record<AuditAction, string> = {
|
||||
'CREATE': 'Created',
|
||||
@@ -198,55 +103,17 @@ function generateDescription(
|
||||
};
|
||||
|
||||
const tableLabels: Record<string, string> = {
|
||||
'users': 'user',
|
||||
'roles': 'role',
|
||||
'meters': 'meter',
|
||||
'concentrators': 'concentrator',
|
||||
'gateways': 'gateway',
|
||||
'devices': 'device',
|
||||
'projects': 'project',
|
||||
'readings': 'reading',
|
||||
'bulk_operations': 'bulk operation',
|
||||
};
|
||||
|
||||
const actionLabel = actionDescriptions[action] || action;
|
||||
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 (tableName === 'unknown' && section) {
|
||||
return `${actionLabel} record in ${section} (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}`;
|
||||
}
|
||||
|
||||
@@ -256,11 +123,6 @@ export function auditMiddleware(
|
||||
next: NextFunction
|
||||
): void {
|
||||
|
||||
if (shouldExclude(req.path)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.path.startsWith('/api')) {
|
||||
next();
|
||||
return;
|
||||
@@ -286,24 +148,18 @@ export function auditMiddleware(
|
||||
return;
|
||||
}
|
||||
|
||||
const action = methodToAction(req.method);
|
||||
const fullPath = (req.originalUrl || req.url).split('?')[0];
|
||||
const section = extractSection(fullPath);
|
||||
const tableName = extractTableName(fullPath);
|
||||
|
||||
const allowedTables = ['concentrators', 'meters'];
|
||||
if (!allowedTables.includes(tableName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = methodToAction(req.method);
|
||||
const recordId = extractRecordId(req);
|
||||
const success = res.statusCode >= 200 && res.statusCode < 400;
|
||||
|
||||
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}`;
|
||||
}
|
||||
const description = generateDescription(action, tableName, recordId);
|
||||
|
||||
let newValues: unknown = undefined;
|
||||
let oldValues: unknown = undefined;
|
||||
|
||||
Reference in New Issue
Block a user