cash-report-system/backend/routes/users.js

258 lines
7.8 KiB
JavaScript

const express = require("express");
const bcrypt = require("bcrypt");
const { body, param, validationResult } = require("express-validator");
const { db } = require("../database/init");
const verifyToken = require("../middleware/auth");
const router = express.Router();
// GET /api/users - admin only
// router.get("/", verifyToken, (req, res) => {
// if (req.user.role !== "admin")
// return res.status(403).json({ error: "Admin only" });
// db.all(
// "SELECT id, username, fullName, role, createdAt, updatedAt FROM users",
// [],
// (err, rows) => {
// if (err) return res.status(500).json({ error: "Database error" });
// res.json({ users: rows });
// }
// );
// });
// GET /api/users - admin only, with stores array
router.get("/", verifyToken, (req, res) => {
if (req.user.role !== "admin")
return res.status(403).json({ error: "Admin only" });
// Get all users
db.all(
"SELECT id, username, password, plaintextPassword, fullName, role, createdAt, updatedAt FROM users",
[],
(err, users) => {
if (err) return res.status(500).json({ error: "Database error" });
// Get user_store_access for all users
db.all(
"SELECT userId, storeId FROM user_store_access",
[],
(err2, accessRows) => {
if (err2)
return res.status(500).json({ error: "Database error (access)" });
// Map userId to storeIds array
const accessMap = {};
accessRows.forEach(({ userId, storeId }) => {
if (!accessMap[userId]) accessMap[userId] = [];
accessMap[userId].push(storeId);
});
// Attach .stores property to each user (empty array if no access)
const usersWithStores = users.map((u) => ({
...u,
stores: accessMap[u.id] || [],
}));
res.json({ users: usersWithStores });
}
);
}
);
});
// POST /api/users - admin only, create user
router.post(
"/",
verifyToken,
[
body("username")
.isString()
.trim()
.isLength({ min: 3 })
.withMessage("Username required (min 3)"),
body("password")
.isString()
.isLength({ min: 6 })
.withMessage("Password required (min 6)"),
// body("fullName")
// .isString()
// .isLength({ min: 1 })
// .withMessage("Full name required"),
body("role").optional().isIn(["admin", "employee", "manager"]),
body("storeIds")
.optional()
.isArray()
.withMessage("storeIds must be an array of store IDs"),
body("storeIds.*")
.optional()
.isInt({ min: 1 })
.withMessage("Each storeId must be a positive integer"),
],
async (req, res) => {
if (req.user.role !== "admin")
return res.status(403).json({ error: "Admin only" });
const errors = validationResult(req);
if (!errors.isEmpty())
return res.status(400).json({ errors: errors.array() });
const { username, password, role = "employee", storeIds = [] } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
// Insert user into users table
db.run(
"INSERT INTO users (username, password, plaintextPassword, role) VALUES (?, ?, ?, ?)",
[username, hashedPassword, password, role],
function (err) {
if (err) {
if (err.message && err.message.includes("UNIQUE constraint failed")) {
return res.status(409).json({ error: "Username already exists" });
}
return res.status(500).json({ error: "Database error" });
}
const userId = this.lastID;
// If storeIds provided, insert access rows
if (Array.isArray(storeIds) && storeIds.length > 0) {
const placeholders = storeIds.map(() => "(?, ?)").join(",");
const accessValues = storeIds.flatMap((storeId) => [userId, storeId]);
db.run(
`INSERT INTO user_store_access (userId, storeId) VALUES ${placeholders}`,
accessValues,
function (err2) {
if (err2)
return res
.status(500)
.json({ error: "Failed to assign store access" });
res.status(201).json({ id: userId });
}
);
} else {
res.status(201).json({ id: userId });
}
}
);
}
);
//PUT /api/users/:id - admin only, update user (update username, password, role, accesible stores)
router.put(
"/:id",
verifyToken,
[
param("id").isInt(),
body("username").optional().isString().isLength({ min: 3 }),
body("password").optional().isString().isLength({ min: 6 }),
body("role").optional().isIn(["admin", "employee", "manager"]),
// body("fullName").optional().isString().isLength({ min: 1 }),
body("storeIds")
.optional()
.isArray()
.withMessage("storeIds must be an array of store IDs"),
body("storeIds.*")
.isInt({ min: 1 })
.withMessage("storeId must be an integer"),
],
async (req, res) => {
if (req.user.role !== "admin")
return res.status(403).json({ error: "Admin only" });
const errors = validationResult(req);
if (!errors.isEmpty())
return res.status(400).json({ errors: errors.array() });
const userId = req.params.id;
const { username, password, role, storeIds } = req.body;
const fields = [];
const values = [];
if (username) {
fields.push("username = ?");
values.push(username);
}
if (role) {
fields.push("role = ?");
values.push(role);
}
if (password) {
const hashedPassword = await bcrypt.hash(password, 10);
fields.push("password = ?");
fields.push("plaintextPassword = ?");
values.push(hashedPassword);
values.push(password);
}
if (fields.length === 0 && !Array.isArray(storeIds))
return res.status(400).json({ error: "No data to update" });
if (fields.length > 0) {
values.push(userId);
await new Promise((resolve, reject) => {
db.run(
`UPDATE users SET ${fields.join(
", "
)}, updatedAt = CURRENT_TIMESTAMP WHERE id = ?`,
values,
function (err) {
if (err) {
if (
err.message &&
err.message.includes("UNIQUE constraint failed")
) {
return res
.status(409)
.json({ error: "Username already exists" });
}
return reject(err);
}
resolve();
}
);
});
}
if (Array.isArray(storeIds)) {
await new Promise((resolve, reject) => {
db.run(
"DELETE FROM user_store_access WHERE userId = ?",
[userId],
(err) => {
if (err) return reject(err);
if (storeIds.length === 0) return resolve();
const placeholders = storeIds.map(() => "(?, ?)").join(",");
const accessValues = storeIds.flatMap((storeId) => [
userId,
storeId,
]);
db.run(
`INSERT INTO user_store_access (userId, storeId) VALUES ${placeholders}`,
accessValues,
(err2) => {
if (err2) return reject(err2);
resolve();
}
);
}
);
});
}
res.json({ success: true });
}
);
//DELETE /api/users/:id - admin only
router.delete("/:id", verifyToken, [param("id").isInt()], (req, res) => {
if (req.user.role !== "admin")
return res.status(403).json({ error: "Admin only" });
if (req.user.userId === parseInt(req.params.id, 10)) {
return res.status(400).json({ error: "Cannot delete yourself" });
}
db.run("DELETE FROM users WHERE id = ?", [req.params.id], function (err) {
if (err) return res.status(500).json({ error: "Database error" });
res.json({ deleted: this.changes });
});
});
module.exports = router;