fix: 12 issues - admin permission scope, player sidebar nav, SMTP label, user create servers

This commit is contained in:
2026-07-16 23:04:15 +08:00
parent e46dad9f95
commit 09f0d8f2aa
4 changed files with 31 additions and 16 deletions

View File

@@ -24,6 +24,7 @@ router.post('/', authenticate, requireRole('owner','admin'), async (req, res) =>
const { username, password, email, game_name, game_uid, role, source } = req.body;
if (!username||!password||!email||!game_name||!game_uid||!role) return res.status(400).json({ error: '所有字段必填' });
if (!['owner','admin','player'].includes(role)) return res.status(400).json({ error: '无效角色' });
if (role === 'owner' && req.user.role !== 'owner') return res.status(403).json({ error: '仅服主可创建服主账号' });
if (password.length < 6) return res.status(400).json({ error: '密码至少6位' });
if (await getRow('SELECT id FROM users WHERE username = ?', [username])) return res.status(400).json({ error: '用户名已存在' });
const hashed = bcrypt.hashSync(password, 10);

View File

@@ -1,17 +1,17 @@
const App = {
navItems: [
{ id: 'dashboard', label: '控制台', icon: 'fa-chart-simple', roles: ['owner','admin','player'] },
{ id: 'unclaimed', label: '待处理工单', icon: 'fa-inbox', roles: ['owner','admin'] },
{ id: 'tickets-create', label: '提交工单', icon: 'fa-plus-circle', roles: ['player'] },
{ id: 'tickets', label: '工单列表', icon: 'fa-ticket-alt', roles: ['owner','admin','player'] },
{ id: 'tracking', label: '追踪工单', icon: 'fa-search', roles: ['owner','admin','player'] },
{ id: 'users', label: '用户管理', icon: 'fa-users', roles: ['owner','admin'] },
{ id: 'notifications', label: '通知配置', icon: 'fa-bell', roles: ['owner','admin'] },
{ id: 'templates', label: '邮件模板', icon: 'fa-envelope', roles: ['owner','admin'] },
{ id: 'settings', label: '系统设置', icon: 'fa-cog', roles: ['owner','admin'] },
{ id: 'unclaimed', label: '待处理工单', icon: 'fa-inbox', roles: ['owner','admin'] },
{ id: 'users', label: '用户管理', icon: 'fa-users', roles: ['owner'] },
{ id: 'servers', label: '服务器管理', icon: 'fa-server', roles: ['owner'] },
{ id: 'polls', label: '投票', icon: 'fa-poll', roles: ['owner'] },
{ id: 'features', label: '更新内容', icon: 'fa-lightbulb', roles: ['owner'] },
{ id: 'notifications', label: '通知配置', icon: 'fa-bell', roles: ['owner'] },
{ id: 'templates', label: '邮件模板', icon: 'fa-envelope', roles: ['owner'] },
{ id: 'settings', label: '系统设置', icon: 'fa-cog', roles: ['owner'] },
{ id: 'export', label: '数据导出', icon: 'fa-download', roles: ['owner'] },
{ id: 'polls', label: '投票', icon: 'fa-poll', roles: ['owner','admin','player'] },
{ id: 'features', label: '更新内容', icon: 'fa-lightbulb', roles: ['owner','admin','player'] },
{ id: 'servers', label: '服务器管理', icon: 'fa-server', roles: ['owner','admin'] },
],
async init() {
@@ -81,6 +81,7 @@ const App = {
switch (page) {
case 'dashboard': this.renderMain('控制台', Dashboard, param); break;
case 'unclaimed': this.renderMain('待处理工单', UnclaimedPage, param); break;
case 'tickets-create': App.navigate('tickets','create?type=report'); break;
case 'tickets':
if (param === 'create') {
const q = new URLSearchParams((location.hash.split('?')[1] || ''));
@@ -120,10 +121,9 @@ const App = {
if (Auth.logged()) {
const u = Auth.user();
el.innerHTML = `<span class="tm ts">${U.esc(u.game_name||u.username)}</span>
<a href="#/dashboard" class="btn btn-p btn-sm"><i class="fas fa-chart-simple"></i> 控制台</a>
<a href="#/polls" class="btn btn-o btn-sm"><i class="fas fa-poll"></i> 投票</a>
<a href="#/features" class="btn btn-o btn-sm"><i class="fas fa-lightbulb"></i> 更新</a>
${Auth.isAdmin() ? `<a href="#/unclaimed" class="btn btn-o btn-sm">待处理</a>`:''}
${Auth.isAdmin() ? `<a href="#/dashboard" class="btn btn-p btn-sm"><i class="fas fa-chart-simple"></i> 控制台</a>` : ''}
${Auth.isAdmin() ? `<a href="#/unclaimed" class="btn btn-o btn-sm">待处理</a>` : ''}
${Auth.isOwner() ? `<a href="#/polls" class="btn btn-o btn-sm"><i class="fas fa-poll"></i></a>` : ''}
<button class="btn btn-o btn-sm" onclick="App.logout()"><i class="fas fa-sign-out-alt"></i> 退出</button>`;
} else {
el.innerHTML = `<a href="#/login" class="btn btn-o btn-sm"><i class="fas fa-sign-in-alt"></i> 登录</a><a href="#/register" class="btn btn-p btn-sm"><i class="fas fa-user-plus"></i> 注册</a>`;
@@ -136,7 +136,7 @@ const App = {
document.getElementById('public-page').classList.add('hidden');
document.getElementById('main-layout').classList.remove('hidden');
const u = Auth.user();
document.getElementById('user-info').innerHTML = `<div class="av">${U.esc(u.game_name||u.username).charAt(0).toUpperCase()}</div><div class="dt"><div class="nm">${U.esc(u.game_name||u.username)}</div><div class="rl">${U.ROLE[u.role]}</div></div>`;
document.getElementById('user-info').innerHTML = `<div class="dt"><div class="nm">${U.esc(u.game_name||u.username)}</div></div>`;
document.getElementById('sidebar-logout').onclick = () => App.logout();
this.loadFooter();
},

View File

@@ -25,7 +25,7 @@ const SettingsPage = {
<div class="grid-2"><div class="form-group"><label>用户名</label><input id="s-user" value="${U.esc(settings.smtp_user?.value||'')}"></div>
<div class="form-group"><label>密码</label><input type="password" id="s-pass" value="${U.esc(settings.smtp_pass?.value||'')}"></div></div>
<div class="grid-2"><div class="form-group"><label>发件地址</label><input id="s-from" value="${U.esc(settings.smtp_from?.value||'')}"></div>
<div class="form-group"><label>SSL</label><select id="s-secure"><option value="0" ${settings.smtp_secure?.value!=='1'?'selected':''}>(587)</option><option value="1" ${settings.smtp_secure?.value==='1'?'selected':''}>(465)</option></select></div></div>
<div class="form-group"><label>加密方式</label><select id="s-secure"><option value="0" ${settings.smtp_secure?.value!=='1'?'selected':''}>STARTTLS (587)</option><option value="1" ${settings.smtp_secure?.value==='1'?'selected':''}>SSL/TLS (465)</option></select></div></div>
<button type="submit" class="btn btn-p btn-sm"><i class="fas fa-save"></i> 保存SMTP设置</button>
</form>
</div></div>

View File

@@ -30,23 +30,37 @@ const UsersPage = {
},
createUser() {
this.loadServersForCreate();
},
async loadServersForCreate() {
let groups = [];
try { groups = await API.get('/polls/groups'); } catch {}
const checks = groups.map(g => {
return `<label style="display:inline-flex;align-items:center;gap:4px;margin:2px 8px 2px 0;font-size:12px;white-space:nowrap"><input type="checkbox" value="${g.group_name}::${g.server_name}" class="cu-srv"> ${g.group_name}/${g.server_name}</label>`;
}).join('');
U.modal('添加用户', `
<form id="cu-form">
<div class="grid-2"><div class="form-group"><label>用户名 *</label><input id="cu-u" required></div><div class="form-group"><label>密码 *</label><input type="password" id="cu-p" required minlength="6"></div></div>
<div class="grid-2"><div class="form-group"><label>邮箱 *</label><input type="email" id="cu-e" required></div><div class="form-group"><label>来源</label><select id="cu-s"><option value="netease">网易端</option><option value="skin">皮肤站</option></select></div></div>
<div class="grid-2"><div class="form-group"><label>角色 *</label><select id="cu-r"><option value="owner">服主</option><option value="admin">管理员</option><option value="player">玩家</option></select></div></div>
<div class="grid-2"><div class="form-group"><label>角色 *</label><select id="cu-r" onchange="this.value==='admin'?document.getElementById('cu-servers').classList.remove('hidden'):document.getElementById('cu-servers').classList.add('hidden')">${req.user.role==='owner'?'<option value="owner">服主</option>':''}<option value="admin">管理员</option><option value="player">玩家</option></select></div></div>
<div class="grid-2"><div class="form-group"><label>游戏名 *</label><input id="cu-gn" required></div><div class="form-group"><label>游戏UID *</label><input id="cu-gu" required></div></div>
<div class="form-group hidden" id="cu-servers"><label>管辖子服务器</label><div style="max-height:120px;overflow-y:auto;border:1px solid var(--g200);border-radius:var(--r);padding:8px">${checks||'<span class="tm">暂无可选服务器</span>'}</div></div>
<div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">取消</button><button type="submit" class="btn btn-p">创建</button></div>
</form>
`);
document.getElementById('cu-form').onsubmit = async e => {
e.preventDefault();
const servers = [];
document.querySelectorAll('.cu-srv:checked').forEach(cb => { const [g,s] = cb.value.split('::'); servers.push({group_name:g,server_name:s}); });
try {
await API.post('/users', {
username: document.getElementById('cu-u').value, password: document.getElementById('cu-p').value,
email: document.getElementById('cu-e').value, role: document.getElementById('cu-r').value,
game_name: document.getElementById('cu-gn').value, game_uid: document.getElementById('cu-gu').value,
source: document.getElementById('cu-s').value,
admin_servers: servers,
});
App.closeModal(); this.load();
} catch (ex) { alert(ex.message); }