258 lines
7.8 KiB
JavaScript
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;
|