Добавлено логгирование

This commit is contained in:
Fovway 2025-10-12 21:30:10 +07:00
parent fb81e914a4
commit d23bdc1bfa
12 changed files with 544 additions and 21 deletions

View File

@ -0,0 +1,50 @@
"use strict";
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable("ActivityLogs", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
userId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: "Users",
key: "id",
},
onUpdate: "CASCADE",
onDelete: "CASCADE",
},
action: {
type: Sequelize.STRING,
allowNull: false,
},
details: {
type: Sequelize.TEXT,
allowNull: true,
},
timestamp: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable("ActivityLogs");
},
};

View File

@ -0,0 +1,44 @@
"use strict";
const { Model } = require("sequelize");
module.exports = (sequelize, DataTypes) => {
class ActivityLog extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
this.belongsTo(models.User, { foreignKey: "userId" });
}
}
ActivityLog.init(
{
userId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: "Users",
key: "id",
},
},
action: {
type: DataTypes.STRING,
allowNull: false,
},
details: {
type: DataTypes.TEXT,
allowNull: true,
},
timestamp: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
},
},
{
sequelize,
modelName: "ActivityLog",
}
);
return ActivityLog;
};

View File

@ -10,6 +10,7 @@ module.exports = (sequelize, DataTypes) => {
*/ */
static associate(models) { static associate(models) {
this.hasMany(models.TimeEntry, { foreignKey: "userId" }); this.hasMany(models.TimeEntry, { foreignKey: "userId" });
this.hasMany(models.ActivityLog, { foreignKey: "userId" });
} }
// Instance method to check password // Instance method to check password

View File

@ -0,0 +1,72 @@
const express = require("express");
const { ActivityLog, User } = require("../models");
const { authenticate } = require("../middleware/auth");
const router = express.Router();
// Get all activity logs (admin only)
router.get("/", authenticate, async (req, res) => {
try {
if (req.user.role !== "admin") {
return res.status(403).json({ error: "Access denied" });
}
const logs = await ActivityLog.findAll({
include: [
{
model: User,
attributes: ["id", "username", "role"],
},
],
order: [["timestamp", "DESC"]],
});
res.json(logs);
} catch (error) {
console.error("Error fetching activity logs:", error);
res.status(500).json({ error: "Internal server error" });
}
});
// Get activity logs for managers and users (manager only)
router.get("/managed", authenticate, async (req, res) => {
try {
if (req.user.role !== "manager" && req.user.role !== "admin") {
return res.status(403).json({ error: "Access denied" });
}
const logs = await ActivityLog.findAll({
include: [
{
model: User,
attributes: ["id", "username", "role"],
},
],
where: {
"$User.role$": ["manager", "user"],
},
order: [["timestamp", "DESC"]],
});
res.json(logs);
} catch (error) {
console.error("Error fetching managed activity logs:", error);
res.status(500).json({ error: "Internal server error" });
}
});
// Log an activity (internal function)
const logActivity = async (userId, action, details = null) => {
try {
await ActivityLog.create({
userId,
action,
details,
timestamp: new Date(),
});
} catch (error) {
console.error("Error logging activity:", error);
}
};
module.exports = { router, logActivity };

View File

@ -1,5 +1,6 @@
const express = require("express"); const express = require("express");
const { TimeEntry, User } = require("../models"); const { TimeEntry, User } = require("../models");
const { logActivity } = require("./activityLogs");
const { const {
authenticate, authenticate,
authorizeAdmin, authorizeAdmin,
@ -28,6 +29,14 @@ router.get("/all", authenticate, authorizeManager, async (req, res) => {
include: [{ model: User, attributes: ["username"] }], include: [{ model: User, attributes: ["username"] }],
order: [["date", "DESC"]], order: [["date", "DESC"]],
}); });
// Log the export action
await logActivity(
req.user.id,
"Экспорт общей таблицы",
`Просмотрена общая таблица записей времени`
);
res.json(entries); res.json(entries);
} catch (error) { } catch (error) {
res.status(500).json({ message: "Server error" }); res.status(500).json({ message: "Server error" });
@ -48,6 +57,18 @@ router.get(
order: [["date", "DESC"]], order: [["date", "DESC"]],
}); });
console.log("Found entries:", entries); console.log("Found entries:", entries);
// Get username for logging
const user = await User.findByPk(userId);
const username = user ? user.username : "Unknown";
// Log the export action for specific user
await logActivity(
req.user.id,
"Экспорт таблицы пользователя",
`Просмотрена таблица записей времени для пользователя: ${username}`
);
res.json(entries); res.json(entries);
} catch (error) { } catch (error) {
console.error("Error fetching user entries:", error); console.error("Error fetching user entries:", error);
@ -100,6 +121,14 @@ router.delete("/delete-all", authenticate, async (req, res) => {
const deletedCount = await TimeEntry.destroy({ const deletedCount = await TimeEntry.destroy({
where: { userId: req.user.id }, where: { userId: req.user.id },
}); });
// Log the delete all action
await logActivity(
req.user.id,
"Удаление всех записей",
`Удалены все записи времени (${deletedCount} записей)`
);
res.json({ message: "All entries deleted", deletedCount }); res.json({ message: "All entries deleted", deletedCount });
} catch (error) { } catch (error) {
res.status(500).json({ message: "Server error" }); res.status(500).json({ message: "Server error" });

View File

@ -1,5 +1,6 @@
const express = require("express"); const express = require("express");
const { User } = require("../models"); const { User } = require("../models");
const { logActivity } = require("./activityLogs");
const { const {
authenticate, authenticate,
authorizeAdmin, authorizeAdmin,
@ -48,6 +49,14 @@ router.post("/", authenticate, authorizeManager, async (req, res) => {
password, password,
role: userRole, role: userRole,
}); });
// Log the creation action
await logActivity(
req.user.id,
"Создание пользователя",
`Создан новый пользователь: ${username} с ролью ${userRole}`
);
res res
.status(201) .status(201)
.json({ id: user.id, username: user.username, role: user.role }); .json({ id: user.id, username: user.username, role: user.role });
@ -73,6 +82,14 @@ router.put(
} }
user.password = password; user.password = password;
await user.save(); await user.save();
// Log the password reset action
await logActivity(
req.user.id,
"Сброс пароля",
`Сброшен пароль для пользователя: ${user.username}`
);
res.json({ message: "Password updated" }); res.json({ message: "Password updated" });
} catch (error) { } catch (error) {
res.status(500).json({ message: "Server error" }); res.status(500).json({ message: "Server error" });
@ -88,6 +105,14 @@ router.delete("/:id", authenticate, authorizeManager, async (req, res) => {
return res.status(404).json({ message: "User not found" }); return res.status(404).json({ message: "User not found" });
} }
await user.destroy(); await user.destroy();
// Log the deletion action
await logActivity(
req.user.id,
"Удаление пользователя",
`Удален пользователь: ${user.username}`
);
res.json({ message: "User deleted" }); res.json({ message: "User deleted" });
} catch (error) { } catch (error) {
res.status(500).json({ message: "Server error" }); res.status(500).json({ message: "Server error" });

View File

@ -82,6 +82,8 @@ sequelize
app.use("/api/auth", require("./routes/auth")); app.use("/api/auth", require("./routes/auth"));
app.use("/api/users", require("./routes/users")); app.use("/api/users", require("./routes/users"));
app.use("/api/time-entries", require("./routes/timeEntries")); app.use("/api/time-entries", require("./routes/timeEntries"));
const { router: activityLogsRouter } = require("./routes/activityLogs");
app.use("/api/activity-logs", activityLogsRouter);
// Health check // Health check
app.get("/health", (req, res) => res.status(200).json({ status: "OK" })); app.get("/health", (req, res) => res.status(200).json({ status: "OK" }));

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { usersAPI, timeEntriesAPI } from "../services/api"; import { usersAPI, timeEntriesAPI, activityLogsAPI } from "../services/api";
import UserTimeEntriesModal from "./UserTimeEntriesModal"; import UserTimeEntriesModal from "./UserTimeEntriesModal";
import * as XLSX from "xlsx"; import * as XLSX from "xlsx";
@ -16,6 +16,9 @@ const AdminPanel = () => {
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
const [deleteUserId, setDeleteUserId] = useState(null); const [deleteUserId, setDeleteUserId] = useState(null);
const [selectedUser, setSelectedUser] = useState(null); const [selectedUser, setSelectedUser] = useState(null);
const [sortConfig, setSortConfig] = useState({ key: null, direction: "asc" });
const [activityLogs, setActivityLogs] = useState([]);
const [showLogs, setShowLogs] = useState(false);
useEffect(() => { useEffect(() => {
fetchUsers(); fetchUsers();
@ -30,6 +33,15 @@ const AdminPanel = () => {
} }
}; };
const fetchActivityLogs = async () => {
try {
const response = await activityLogsAPI.getLogs();
setActivityLogs(response.data);
} catch (error) {
console.error("Failed to fetch activity logs:", error);
}
};
const handleExportAll = async () => { const handleExportAll = async () => {
try { try {
const response = await timeEntriesAPI.getAllEntries(); const response = await timeEntriesAPI.getAllEntries();
@ -153,6 +165,27 @@ const AdminPanel = () => {
} }
}; };
const handleSort = (key) => {
let direction = "asc";
if (sortConfig.key === key && sortConfig.direction === "asc") {
direction = "desc";
}
setSortConfig({ key, direction });
};
const sortedUsers = [...users].sort((a, b) => {
if (sortConfig.key === null) return 0;
const aValue = a[sortConfig.key];
const bValue = b[sortConfig.key];
if (aValue < bValue) {
return sortConfig.direction === "asc" ? -1 : 1;
}
if (aValue > bValue) {
return sortConfig.direction === "asc" ? 1 : -1;
}
return 0;
});
return ( return (
<div className="w-100 mt-5 px-0"> <div className="w-100 mt-5 px-0">
<h3>Админ панель</h3> <h3>Админ панель</h3>
@ -169,6 +202,15 @@ const AdminPanel = () => {
<button className="btn btn-info" onClick={handleExportAll}> <button className="btn btn-info" onClick={handleExportAll}>
Экспорт общей таблицы Экспорт общей таблицы
</button> </button>
<button
className="btn btn-warning"
onClick={() => {
setShowLogs(!showLogs);
if (!showLogs) fetchActivityLogs();
}}
>
{showLogs ? "Скрыть логи" : "Показать логи"}
</button>
</div> </div>
{showCreateForm && ( {showCreateForm && (
@ -231,14 +273,56 @@ const AdminPanel = () => {
<table className="table table-striped table-hover"> <table className="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th
<th>Имя пользователя</th> onClick={() => handleSort("id")}
<th>Роль</th> style={{
cursor: "pointer",
backgroundColor:
sortConfig.key === "id" ? "#f0f0f0" : "inherit",
color: sortConfig.key === "id" ? "#007bff" : "inherit",
}}
title="Кликни для сортировки"
>
ID{" "}
{sortConfig.key === "id" &&
(sortConfig.direction === "asc" ? "↑" : "↓")}
{sortConfig.key !== "id" && "↕"}
</th>
<th
onClick={() => handleSort("username")}
style={{
cursor: "pointer",
backgroundColor:
sortConfig.key === "username" ? "#f0f0f0" : "inherit",
color: sortConfig.key === "username" ? "#007bff" : "inherit",
}}
title="Кликни для сортировки"
>
Имя пользователя{" "}
{sortConfig.key === "username" &&
(sortConfig.direction === "asc" ? "↑" : "↓")}
{sortConfig.key !== "username" && "↕"}
</th>
<th
onClick={() => handleSort("role")}
style={{
cursor: "pointer",
backgroundColor:
sortConfig.key === "role" ? "#f0f0f0" : "inherit",
color: sortConfig.key === "role" ? "#007bff" : "inherit",
}}
title="Кликни для сортировки"
>
Роль{" "}
{sortConfig.key === "role" &&
(sortConfig.direction === "asc" ? "↑" : "↓")}
{sortConfig.key !== "role" && "↕"}
</th>
<th>Действия</th> <th>Действия</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{users.map((user) => ( {sortedUsers.map((user) => (
<tr key={user.id}> <tr key={user.id}>
<td>{user.id}</td> <td>{user.id}</td>
<td> <td>
@ -393,6 +477,45 @@ const AdminPanel = () => {
</div> </div>
)} )}
{showLogs && (
<div className="mb-4">
<h4>Логи действий</h4>
<div className="table-responsive w-100">
<table className="table table-striped table-hover">
<thead>
<tr>
<th>Время</th>
<th>Пользователь</th>
<th>Роль</th>
<th>Действие</th>
<th>Детали</th>
</tr>
</thead>
<tbody>
{activityLogs.map((log) => (
<tr key={log.id}>
<td>{new Date(log.timestamp).toLocaleString("ru-RU")}</td>
<td>{log.User?.username || "Unknown"}</td>
<td>
{log.User?.role === "manager"
? "Руководство"
: log.User?.role === "admin"
? "Админ"
: "Пользователь"}
</td>
<td>{log.action}</td>
<td>{log.details || "-"}</td>
</tr>
))}
</tbody>
</table>
{activityLogs.length === 0 && (
<div className="text-center text-muted">Нет записей логов</div>
)}
</div>
</div>
)}
{selectedUser && ( {selectedUser && (
<UserTimeEntriesModal <UserTimeEntriesModal
show={!!selectedUser} show={!!selectedUser}

View File

@ -16,6 +16,7 @@ const ManagerPanel = () => {
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
const [deleteUserId, setDeleteUserId] = useState(null); const [deleteUserId, setDeleteUserId] = useState(null);
const [selectedUser, setSelectedUser] = useState(null); const [selectedUser, setSelectedUser] = useState(null);
const [sortConfig, setSortConfig] = useState({ key: null, direction: "asc" });
useEffect(() => { useEffect(() => {
fetchUsers(); fetchUsers();
@ -153,6 +154,27 @@ const ManagerPanel = () => {
} }
}; };
const handleSort = (key) => {
let direction = "asc";
if (sortConfig.key === key && sortConfig.direction === "asc") {
direction = "desc";
}
setSortConfig({ key, direction });
};
const sortedUsers = [...users].sort((a, b) => {
if (sortConfig.key === null) return 0;
const aValue = a[sortConfig.key];
const bValue = b[sortConfig.key];
if (aValue < bValue) {
return sortConfig.direction === "asc" ? -1 : 1;
}
if (aValue > bValue) {
return sortConfig.direction === "asc" ? 1 : -1;
}
return 0;
});
return ( return (
<div className="w-100 mt-5 px-0"> <div className="w-100 mt-5 px-0">
<h3>Менеджер панель</h3> <h3>Менеджер панель</h3>
@ -230,14 +252,56 @@ const ManagerPanel = () => {
<table className="table table-striped table-hover"> <table className="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th
<th>Имя пользователя</th> onClick={() => handleSort("id")}
<th>Роль</th> style={{
cursor: "pointer",
backgroundColor:
sortConfig.key === "id" ? "#f0f0f0" : "inherit",
color: sortConfig.key === "id" ? "#007bff" : "inherit",
}}
title="Кликни для сортировки"
>
ID{" "}
{sortConfig.key === "id" &&
(sortConfig.direction === "asc" ? "↑" : "↓")}
{sortConfig.key !== "id" && "↕"}
</th>
<th
onClick={() => handleSort("username")}
style={{
cursor: "pointer",
backgroundColor:
sortConfig.key === "username" ? "#f0f0f0" : "inherit",
color: sortConfig.key === "username" ? "#007bff" : "inherit",
}}
title="Кликни для сортировки"
>
Имя пользователя{" "}
{sortConfig.key === "username" &&
(sortConfig.direction === "asc" ? "↑" : "↓")}
{sortConfig.key !== "username" && "↕"}
</th>
<th
onClick={() => handleSort("role")}
style={{
cursor: "pointer",
backgroundColor:
sortConfig.key === "role" ? "#f0f0f0" : "inherit",
color: sortConfig.key === "role" ? "#007bff" : "inherit",
}}
title="Кликни для сортировки"
>
Роль{" "}
{sortConfig.key === "role" &&
(sortConfig.direction === "asc" ? "↑" : "↓")}
{sortConfig.key !== "role" && "↕"}
</th>
<th>Действия</th> <th>Действия</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{users.map((user) => ( {sortedUsers.map((user) => (
<tr key={user.id}> <tr key={user.id}>
<td>{user.id}</td> <td>{user.id}</td>
<td> <td>
@ -257,24 +321,28 @@ const ManagerPanel = () => {
</td> </td>
<td> <td>
<div className="d-flex gap-1 flex-column flex-lg-row"> <div className="d-flex gap-1 flex-column flex-lg-row">
<button {user.role !== "admin" && (
className="btn btn-sm btn-warning" <button
onClick={() => setResetPasswordId(user.id)} className="btn btn-sm btn-warning"
> onClick={() => setResetPasswordId(user.id)}
Сбросить пароль >
</button> Сбросить пароль
</button>
)}
<button <button
className="btn btn-sm btn-success" className="btn btn-sm btn-success"
onClick={() => handleExportUser(user.id, user.username)} onClick={() => handleExportUser(user.id, user.username)}
> >
Экспорт Экспорт
</button> </button>
<button {user.role !== "admin" && (
className="btn btn-sm btn-danger" <button
onClick={() => setDeleteUserId(user.id)} className="btn btn-sm btn-danger"
> onClick={() => setDeleteUserId(user.id)}
Удалить >
</button> Удалить
</button>
)}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -38,4 +38,9 @@ export const timeEntriesAPI = {
deleteAllEntries: () => api.delete("/time-entries/delete-all"), deleteAllEntries: () => api.delete("/time-entries/delete-all"),
}; };
export const activityLogsAPI = {
getLogs: () => api.get("/activity-logs"),
getManagedLogs: () => api.get("/activity-logs/managed"),
};
export default api; export default api;

64
test-api.js Normal file
View File

@ -0,0 +1,64 @@
const http = require("http");
function makeRequest(options, postData = null) {
return new Promise((resolve, reject) => {
const req = http.request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
try {
resolve({
statusCode: res.statusCode,
data: JSON.parse(data),
});
} catch (e) {
resolve({
statusCode: res.statusCode,
data: data,
});
}
});
});
req.on("error", (err) => {
reject(err);
});
if (postData) {
req.write(postData);
}
req.end();
});
}
async function testAPI() {
try {
console.log("Testing health endpoint...");
const healthResponse = await makeRequest({
hostname: "localhost",
port: 5000,
path: "/health",
method: "GET",
});
console.log("Health Status:", healthResponse.statusCode);
console.log("Health Data:", healthResponse.data);
console.log(
"\nTesting activity logs endpoint (should return 401 without auth)..."
);
const logsResponse = await makeRequest({
hostname: "localhost",
port: 5000,
path: "/api/activity-logs",
method: "GET",
});
console.log("Activity Logs Status:", logsResponse.statusCode);
console.log("Activity Logs Data:", logsResponse.data);
} catch (error) {
console.error("Error:", error.message);
}
}
testAPI();

40
test-log.js Normal file
View File

@ -0,0 +1,40 @@
require("dotenv").config();
const { ActivityLog, User } = require("./backend/models");
async function createTestLog() {
try {
// Найти админа
const admin = await User.findOne({ where: { role: "admin" } });
if (!admin) {
console.log("Admin not found");
return;
}
// Создать тестовую запись лога
const log = await ActivityLog.create({
userId: admin.id,
action: "Тестовое действие",
details: "Это тестовая запись лога для проверки системы",
timestamp: new Date(),
});
console.log("Test log created:", log.toJSON());
// Получить все логи
const logs = await ActivityLog.findAll({
include: [{ model: User, attributes: ["username", "role"] }],
order: [["timestamp", "DESC"]],
});
console.log("\nAll logs:");
logs.forEach((log) => {
console.log(
`- ${log.User.username} (${log.User.role}): ${log.action} - ${log.details}`
);
});
} catch (error) {
console.error("Error:", error);
}
}
createTestLog();