Skip to content
This repository was archived by the owner on Jul 28, 2026. It is now read-only.

Commit 9ca4a0b

Browse files
committed
feat: add admin notification sending to user inbox
1 parent 057389b commit 9ca4a0b

2 files changed

Lines changed: 92 additions & 0 deletions

File tree

public/js/admin.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,33 @@ async function renderAdminUserDetail(userId) {
911911
<button class="btn ${u.auth_restricted ? 'btn-primary' : 'btn-warning'}" id="admin-btn-toggle-auth-restriction" style="width:auto">${u.auth_restricted ? 'Unrestrict Auth' : 'Restrict Auth'}</button>
912912
</div>
913913
</div>
914+
<div class="card">
915+
<h2 class="card-title" style="margin-bottom:16px;color:var(--accent-1)">Send Notification</h2>
916+
<p style="color:var(--text-secondary);font-size:0.88rem;margin-bottom:12px">
917+
Send a message to this user's notification inbox.
918+
</p>
919+
<form id="admin-notify-form">
920+
<div class="form-group" style="margin-bottom:12px">
921+
<label for="admin-notify-title">Title</label>
922+
<input type="text" id="admin-notify-title" placeholder="e.g. Account Update" style="width:100%" required />
923+
</div>
924+
<div class="form-group" style="margin-bottom:12px">
925+
<label for="admin-notify-message">Message</label>
926+
<textarea id="admin-notify-message" rows="3" placeholder="Write your message..." style="resize:vertical;width:100%;padding:10px 14px;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text-primary);font-family:inherit;font-size:0.88rem;margin-top:6px;box-sizing:border-box" required></textarea>
927+
</div>
928+
<div class="form-group" style="margin-bottom:12px">
929+
<label for="admin-notify-type">Type</label>
930+
<select id="admin-notify-type" style="width:100%">
931+
<option value="info">Info</option>
932+
<option value="success">Success</option>
933+
<option value="warning">Warning</option>
934+
<option value="error">Error</option>
935+
</select>
936+
</div>
937+
<button type="submit" class="btn btn-primary" id="admin-btn-send-notification" style="width:auto">Send</button>
938+
<div id="admin-notify-msg" style="margin-top:8px;display:none"></div>
939+
</form>
940+
</div>
914941
</div>
915942
</div>
916943
@@ -1034,6 +1061,38 @@ function initUserActions(userId) {
10341061
}
10351062
});
10361063

1064+
$a('#admin-notify-form')?.addEventListener('submit', async (e) => {
1065+
e.preventDefault();
1066+
const btn = $a('#admin-btn-send-notification');
1067+
const msgEl = $a('#admin-notify-msg');
1068+
const titleEl = $a('#admin-notify-title');
1069+
const messageEl = $a('#admin-notify-message');
1070+
const typeEl = $a('#admin-notify-type');
1071+
btn.disabled = true;
1072+
btn.innerHTML = '<span class="spinner"></span> Sending...';
1073+
if (msgEl) msgEl.style.display = 'none';
1074+
try {
1075+
await adminApi(`/users/${userId}/notify`, {
1076+
method: 'POST',
1077+
body: JSON.stringify({
1078+
title: titleEl.value.trim(),
1079+
message: messageEl.value.trim(),
1080+
type: typeEl.value,
1081+
}),
1082+
});
1083+
if (msgEl) { msgEl.textContent = 'Notification sent successfully'; msgEl.style.display = 'block'; msgEl.style.color = 'var(--accent-green)'; }
1084+
titleEl.value = '';
1085+
messageEl.value = '';
1086+
typeEl.value = 'info';
1087+
btn.disabled = false;
1088+
btn.innerHTML = 'Send';
1089+
} catch (err) {
1090+
if (msgEl) { msgEl.textContent = err.message; msgEl.style.display = 'block'; msgEl.style.color = 'var(--accent-red)'; }
1091+
btn.disabled = false;
1092+
btn.innerHTML = 'Send';
1093+
}
1094+
});
1095+
10371096
$a('#admin-btn-delete-user')?.addEventListener('click', async () => {
10381097
if (!confirm('Are you sure you want to permanently delete this user? All their servers will be deleted. This action cannot be undone.')) return;
10391098
const btn = $a('#admin-btn-delete-user');

routes/admin.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,39 @@ router.post('/users/:id/toggle-admin', authenticateToken, requireAdmin, async (r
398398
}
399399
});
400400

401+
router.post('/users/:id/notify', authenticateToken, requireAdmin, async (req, res) => {
402+
try {
403+
const userId = parseInt(req.params.id, 10);
404+
if (isNaN(userId)) {
405+
return res.status(400).json({ error: 'Invalid user ID' });
406+
}
407+
408+
const { title, message, type } = req.body;
409+
if (!title || !title.trim()) {
410+
return res.status(400).json({ error: 'Title is required' });
411+
}
412+
if (!message || !message.trim()) {
413+
return res.status(400).json({ error: 'Message is required' });
414+
}
415+
416+
const users = await query('SELECT id FROM users WHERE id = ?', [userId]);
417+
if (users.length === 0) {
418+
return res.status(404).json({ error: 'User not found' });
419+
}
420+
421+
const validTypes = ['info', 'success', 'warning', 'error'];
422+
const notifType = validTypes.includes(type) ? type : 'info';
423+
424+
await createNotification(userId, title.trim(), message.trim(), notifType);
425+
await logActivity(req.user.userId, 'admin_notify', `Sent notification to user #${userId}: ${title.trim()}`, null);
426+
427+
res.json({ success: true });
428+
} catch (err) {
429+
console.error('Admin notify error:', err.message);
430+
res.status(500).json({ error: 'Failed to send notification: ' + err.message });
431+
}
432+
});
433+
401434
router.delete('/users/:id', authenticateToken, requireAdmin, async (req, res) => {
402435
try {
403436
const userId = parseInt(req.params.id, 10);

0 commit comments

Comments
 (0)