Commit 41ae2a28 by tdgiang

Add admin-only account management: create, list, lock/unlock, reset password

Appends accountsList, createAccount, toggleAccount, and resetAccountPassword
to adminAuth.server.controller.js, adds the accounts-list view, and wires
the /admin/accounts* routes behind requireLogin + requireAdmin.
Co-Authored-By: 's avatarClaude Sonnet 5 <noreply@anthropic.com>
parent 467c8600
......@@ -33,3 +33,92 @@ exports.logout = function (req, res) {
res.redirect("/admin/login");
});
};
exports.accountsList = function (req, res) {
AdminUser.find({}).sort({ createdAt: 1 }).exec(function (err, users) {
if (err) {
console.error("accountsList: DB error:", err.message);
return res.status(500).send("Lỗi cơ sở dữ liệu");
}
res.render("admin/accounts-list", {
users: users,
currentUsername: req.session.username,
error: req.query.error || null,
created: req.query.created === "1",
reset: req.query.reset === "1",
});
});
};
exports.createAccount = function (req, res) {
var username = String(req.body.username || "").trim().toLowerCase();
var password = String(req.body.password || "");
var role = req.body.role === "admin" ? "admin" : "staff";
if (!username || !password) {
return res.redirect("/admin/accounts?error=MISSING_FIELDS");
}
AdminUser.create(
{
username: username,
passwordHash: passwordHash.hash(password),
role: role,
active: true,
},
function (err) {
if (err) {
if (err.code === 11000) {
return res.redirect("/admin/accounts?error=DUPLICATE_USERNAME");
}
console.error("createAccount: DB error:", err.message);
return res.redirect("/admin/accounts?error=DB_ERROR");
}
return res.redirect("/admin/accounts?created=1");
}
);
};
exports.toggleAccount = function (req, res) {
AdminUser.findById(req.params.id, function (err, user) {
if (err) {
console.error("toggleAccount: DB error:", err.message);
return res.redirect("/admin/accounts?error=DB_ERROR");
}
if (!user) {
return res.status(404).send("Không tìm thấy tài khoản");
}
user.active = !user.active;
user.save(function (saveErr) {
if (saveErr) {
console.error("toggleAccount: DB error saving:", saveErr.message);
return res.redirect("/admin/accounts?error=DB_ERROR");
}
return res.redirect("/admin/accounts");
});
});
};
exports.resetAccountPassword = function (req, res) {
var newPassword = String(req.body.newPassword || "");
if (!newPassword) {
return res.redirect("/admin/accounts?error=MISSING_FIELDS");
}
AdminUser.findById(req.params.id, function (err, user) {
if (err) {
console.error("resetAccountPassword: DB error:", err.message);
return res.redirect("/admin/accounts?error=DB_ERROR");
}
if (!user) {
return res.status(404).send("Không tìm thấy tài khoản");
}
user.passwordHash = passwordHash.hash(newPassword);
user.save(function (saveErr) {
if (saveErr) {
console.error("resetAccountPassword: DB error saving:", saveErr.message);
return res.redirect("/admin/accounts?error=DB_ERROR");
}
return res.redirect("/admin/accounts?reset=1");
});
});
};
......@@ -3,6 +3,7 @@ module.exports = function (app) {
var admin = require("../../app/controllers/admin.server.controller");
var adminAuth = require("../../app/controllers/adminAuth.server.controller");
var requireLogin = require("../middlewares/requireLogin");
var requireAdmin = require("../middlewares/requireAdmin");
app.route("/admin/login")
.get(adminAuth.loginForm)
......@@ -10,6 +11,13 @@ module.exports = function (app) {
app.route("/admin/logout").post(requireLogin, adminAuth.logout);
app.route("/admin/accounts")
.get(requireLogin, requireAdmin, adminAuth.accountsList)
.post(requireLogin, requireAdmin, adminAuth.createAccount);
app.route("/admin/accounts/:id/toggle").post(requireLogin, requireAdmin, adminAuth.toggleAccount);
app.route("/admin/accounts/:id/reset-password").post(requireLogin, requireAdmin, adminAuth.resetAccountPassword);
app.route("/admin/transactions")
.get(requireLogin, admin.listTransactions)
.post(requireLogin, admin.createTransaction);
......
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Quản lý tài khoản</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--color-bg: #F1F5F9;
--color-surface: #FFFFFF;
--color-primary: #1E40AF;
--color-primary-hover: #1D4ED8;
--color-text: #0F172A;
--color-text-muted: #64748B;
--color-border: #E2E8F0;
--color-header-bg: #F8FAFC;
--pill-admin-bg: #DBEAFE;
--pill-admin-fg: #1E3A8A;
--pill-staff-bg: #F1F5F9;
--pill-staff-fg: #334155;
--pill-active-bg: #DCFCE7;
--pill-active-fg: #166534;
--pill-locked-bg: #FEE2E2;
--pill-locked-fg: #991B1B;
--color-success-bg: #F0FDF4;
--color-success-border: #BBF7D0;
--color-danger-bg: #FEF2F2;
--color-danger-border: #FECACA;
--radius: 10px;
--shadow-card: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.06);
}
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh; background: var(--color-bg);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
color: var(--color-text); padding: 40px 20px;
}
.page { max-width: 900px; margin: 0 auto; }
.nav { display: flex; gap: 16px; font-size: 13px; margin-bottom: 20px; }
.nav a { color: var(--color-text-muted); text-decoration: none; font-weight: 500; }
.nav a:hover { color: var(--color-primary); }
h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.01em; margin: 0 0 20px; }
.card {
background: var(--color-surface); border: 1px solid var(--color-border);
border-radius: var(--radius); box-shadow: var(--shadow-card); padding: 24px; margin-bottom: 24px;
}
.card h2 { font-size: 15px; font-weight: 700; margin: 0 0 16px; }
.form-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; }
.form-field { flex: 1; min-width: 160px; }
label { display: block; font-size: 12.5px; font-weight: 600; margin-bottom: 6px; }
input, select {
width: 100%; height: 40px; padding: 0 12px; font-size: 14px; font-family: inherit;
border: 1px solid var(--color-border); border-radius: 8px; outline: none;
}
input:focus, select:focus { border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.12); }
button {
height: 40px; padding: 0 18px; background: var(--color-primary); color: #fff;
font-family: inherit; font-size: 14px; font-weight: 600; border: none; border-radius: 8px; cursor: pointer;
}
button:hover { background: var(--color-primary-hover); }
button.secondary { background: #fff; color: var(--color-text); border: 1px solid var(--color-border); }
button.secondary:hover { background: var(--color-header-bg); }
.alert { padding: 12px 14px; border-radius: 8px; font-size: 13.5px; margin-bottom: 20px; border: 1px solid; }
.alert-success { background: var(--color-success-bg); border-color: var(--color-success-border); color: #14532D; }
.alert-error { background: var(--color-danger-bg); border-color: var(--color-danger-border); color: #7F1D1D; }
table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
thead th {
text-align: left; font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.03em;
color: var(--color-text-muted); padding: 10px 12px; border-bottom: 1px solid var(--color-border);
}
tbody td { padding: 12px; border-bottom: 1px solid var(--color-border); vertical-align: middle; }
tbody tr:last-child td { border-bottom: none; }
.pill { display: inline-flex; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; }
.pill-admin { background: var(--pill-admin-bg); color: var(--pill-admin-fg); }
.pill-staff { background: var(--pill-staff-bg); color: var(--pill-staff-fg); }
.pill-active { background: var(--pill-active-bg); color: var(--pill-active-fg); }
.pill-locked { background: var(--pill-locked-bg); color: var(--pill-locked-fg); }
.row-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.row-actions form { display: inline-flex; gap: 6px; align-items: center; }
.row-actions input[type="password"] { height: 32px; width: 130px; font-size: 12.5px; }
.row-actions button { height: 32px; padding: 0 10px; font-size: 12.5px; }
</style>
</head>
<body>
<div class="page">
<div class="nav">
<a href="/admin/transactions">&laquo; Lịch sử giao dịch</a>
<a href="/admin/account/password">Đổi mật khẩu</a>
<form method="POST" action="/admin/logout" style="display:inline;">
<button type="submit" style="all:unset;cursor:pointer;color:inherit;font:inherit;">Đăng xuất</button>
</form>
</div>
<h1>Quản lý tài khoản</h1>
{% if created %}<div class="alert alert-success">Đã tạo tài khoản thành công.</div>{% endif %}
{% if reset %}<div class="alert alert-success">Đã đặt lại mật khẩu.</div>{% endif %}
{% if error == "MISSING_FIELDS" %}<div class="alert alert-error">Vui lòng nhập đủ thông tin.</div>{% endif %}
{% if error == "DUPLICATE_USERNAME" %}<div class="alert alert-error">Tên đăng nhập đã tồn tại.</div>{% endif %}
{% if error == "DB_ERROR" %}<div class="alert alert-error">Lỗi cơ sở dữ liệu, vui lòng thử lại.</div>{% endif %}
<div class="card">
<h2>Tạo tài khoản nhân viên mới</h2>
<form method="POST" action="/admin/accounts">
<div class="form-row">
<div class="form-field">
<label for="username">Tên đăng nhập</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-field">
<label for="password">Mật khẩu</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-field" style="max-width:140px;">
<label for="role">Vai trò</label>
<select id="role" name="role">
<option value="staff">Nhân viên</option>
<option value="admin">Admin</option>
</select>
</div>
<button type="submit">Tạo tài khoản</button>
</div>
</form>
</div>
<div class="card">
<h2>Danh sách tài khoản</h2>
<table>
<thead>
<tr>
<th>Tên đăng nhập</th>
<th>Vai trò</th>
<th>Trạng thái</th>
<th>Thao tác</th>
</tr>
</thead>
<tbody>
{% for u in users %}
<tr>
<td>{{u.username}}</td>
<td><span class="pill pill-{{u.role}}">{% if u.role == "admin" %}Admin{% else %}Nhân viên{% endif %}</span></td>
<td><span class="pill pill-{% if u.active %}active{% else %}locked{% endif %}">{% if u.active %}Hoạt động{% else %}Đã khoá{% endif %}</span></td>
<td>
<div class="row-actions">
<form method="POST" action="/admin/accounts/{{u._id}}/toggle">
<button type="submit" class="secondary">{% if u.active %}Khoá{% else %}Mở khoá{% endif %}</button>
</form>
<form method="POST" action="/admin/accounts/{{u._id}}/reset-password">
<input type="password" name="newPassword" placeholder="Mật khẩu mới" required>
<button type="submit" class="secondary">Đặt lại</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment