Добавлено логгирование
This commit is contained in:
parent
fb81e914a4
commit
d23bdc1bfa
50
backend/migrations/20251012140844-create-activity-logs.js
Normal file
50
backend/migrations/20251012140844-create-activity-logs.js
Normal 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");
|
||||||
|
},
|
||||||
|
};
|
||||||
44
backend/models/activitylog.js
Normal file
44
backend/models/activitylog.js
Normal 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;
|
||||||
|
};
|
||||||
@ -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
|
||||||
|
|||||||
72
backend/routes/activityLogs.js
Normal file
72
backend/routes/activityLogs.js
Normal 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 };
|
||||||
@ -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" });
|
||||||
|
|||||||
@ -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" });
|
||||||
|
|||||||
@ -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" }));
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
64
test-api.js
Normal 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
40
test-log.js
Normal 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();
|
||||||
Loading…
x
Reference in New Issue
Block a user