passwords
This commit is contained in:
1 parent
5ac5626406
commit
6029d6f244
3 files changed
+165
No files matched your search
@@ -62,6 +62,7 @@
|
|||||||
<button onclick="switchTab('tabReports')">📘 Reports</button>
|
<button onclick="switchTab('tabReports')">📘 Reports</button>
|
||||||
<button onclick="switchTab('tabSessions')">📅 Sessions</button>
|
<button onclick="switchTab('tabSessions')">📅 Sessions</button>
|
||||||
<button onclick="switchTab('tabBadges')">🏅 Badges</button>
|
<button onclick="switchTab('tabBadges')">🏅 Badges</button>
|
||||||
|
<button onclick="switchTab('tabPasswords')">🔐 Passwords</button>
|
||||||
<button onclick="switchTab('tabTheme')">🎨 Themes</button>
|
<button onclick="switchTab('tabTheme')">🎨 Themes</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -186,6 +187,46 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Theme Tab -->
|
<!-- Theme Tab -->
|
||||||
|
<!-- Passwords Tab -->
|
||||||
|
<div class="adminTab hidden" id="tabPasswords">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>🔐 Password Management</h3>
|
||||||
|
<p style="color: var(--text-secondary);">Change user passwords</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
||||||
|
<!-- Change Password Form -->
|
||||||
|
<div class="card">
|
||||||
|
<h4>Change User Password</h4>
|
||||||
|
<form onsubmit="event.preventDefault(); changeUserPassword();" style="display: flex; flex-direction: column; gap: 1rem;">
|
||||||
|
<label for="passwordUsername">Select User:</label>
|
||||||
|
<select id="passwordUsername" required style="width: 100%;">
|
||||||
|
<option value="">-- Select a user --</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="newUserPassword">New Password:</label>
|
||||||
|
<input id="newUserPassword" placeholder="Enter new password" required type="password"/>
|
||||||
|
|
||||||
|
<label for="confirmPassword">Confirm Password:</label>
|
||||||
|
<input id="confirmPassword" placeholder="Confirm new password" required type="password"/>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-warning">🔐 Change Password</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password History -->
|
||||||
|
<div class="card">
|
||||||
|
<h4>Password Change Log</h4>
|
||||||
|
<div id="passwordLog" style="max-height: 300px; overflow-y: auto; background: var(--bg-secondary); padding: 1rem; border-radius: var(--radius);">
|
||||||
|
<p style="color: var(--text-secondary); font-style: italic;">No password changes recorded yet.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Themes Tab -->
|
||||||
<div class="adminTab hidden" id="tabTheme">
|
<div class="adminTab hidden" id="tabTheme">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>🎨 Application Themes</h3>
|
<h3>🎨 Application Themes</h3>
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ window.switchTab = function switchTab(targetId) {
|
|||||||
loadStudentNamesForReport();
|
loadStudentNamesForReport();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'tabPasswords':
|
||||||
|
populatePasswordUserDropdown();
|
||||||
|
break;
|
||||||
case 'tabBadges':
|
case 'tabBadges':
|
||||||
if (typeof loadBadgeViewer === 'function') {
|
if (typeof loadBadgeViewer === 'function') {
|
||||||
loadBadgeViewer();
|
loadBadgeViewer();
|
||||||
@@ -575,6 +578,102 @@ function handleFileUpload(event) {
|
|||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔐 Password Management Functions
|
||||||
|
window.changeUserPassword = async function() {
|
||||||
|
const username = document.getElementById('passwordUsername').value;
|
||||||
|
const newPassword = document.getElementById('newUserPassword').value;
|
||||||
|
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
alert('Please select a user');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newPassword || newPassword.length < 3) {
|
||||||
|
alert('Password must be at least 3 characters long');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
alert('Passwords do not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hash the new password (same way as registration)
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(newPassword);
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const newPasswordHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
|
||||||
|
// Send password change request
|
||||||
|
const response = await fetch('/changePassword', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, newPasswordHash })
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = await response.text();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert(message);
|
||||||
|
logPasswordChange(username);
|
||||||
|
// Clear form
|
||||||
|
document.getElementById('newUserPassword').value = '';
|
||||||
|
document.getElementById('confirmPassword').value = '';
|
||||||
|
document.getElementById('passwordUsername').value = '';
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error changing password:', error);
|
||||||
|
alert('Failed to change password');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log password changes
|
||||||
|
function logPasswordChange(username) {
|
||||||
|
const passwordLog = document.getElementById('passwordLog');
|
||||||
|
const timestamp = new Date().toLocaleString();
|
||||||
|
|
||||||
|
const logEntry = document.createElement('div');
|
||||||
|
logEntry.style.padding = '0.5rem';
|
||||||
|
logEntry.style.marginBottom = '0.5rem';
|
||||||
|
logEntry.style.backgroundColor = 'var(--bg-primary)';
|
||||||
|
logEntry.style.borderRadius = 'var(--radius)';
|
||||||
|
logEntry.style.borderLeft = '4px solid var(--warning)';
|
||||||
|
|
||||||
|
logEntry.innerHTML = `
|
||||||
|
<strong>🔐 Password Changed</strong><br>
|
||||||
|
<span style="color: var(--text-secondary);">User: ${username}</span><br>
|
||||||
|
<span style="color: var(--text-secondary); font-size: 0.9rem;">${timestamp}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Remove "no changes" message if it exists
|
||||||
|
if (passwordLog.querySelector('p')) {
|
||||||
|
passwordLog.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new entry at the top
|
||||||
|
passwordLog.insertBefore(logEntry, passwordLog.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate password username dropdown
|
||||||
|
function populatePasswordUserDropdown() {
|
||||||
|
const dropdown = document.getElementById('passwordUsername');
|
||||||
|
if (!dropdown || !adminUsers) return;
|
||||||
|
|
||||||
|
dropdown.innerHTML = '<option value="">-- Select a user --</option>';
|
||||||
|
|
||||||
|
adminUsers.forEach(user => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = user.username;
|
||||||
|
option.textContent = `${user.username} (${user.role})`;
|
||||||
|
dropdown.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add event listeners for admin tabs as backup to onclick
|
// Add event listeners for admin tabs as backup to onclick
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Setup tab click listeners
|
// Setup tab click listeners
|
||||||
|
|||||||
@@ -110,6 +110,31 @@ app.post('/deleteUser', (req, res) => {
|
|||||||
res.send(`✅ User "${username}" deleted`);
|
res.send(`✅ User "${username}" deleted`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🔐 Change user password
|
||||||
|
app.post('/changePassword', (req, res) => {
|
||||||
|
const { username, newPasswordHash } = req.body;
|
||||||
|
if (typeof username !== 'string' || typeof newPasswordHash !== 'string') {
|
||||||
|
return res.status(400).send("Invalid request data");
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersPath = path.join(DATA_DIR, files.users);
|
||||||
|
const users = readJsonSafe(usersPath, []);
|
||||||
|
const userIndex = users.findIndex(u => u.username === username);
|
||||||
|
|
||||||
|
if (userIndex === -1) {
|
||||||
|
return res.status(404).send("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the password hash
|
||||||
|
users[userIndex].hash = newPasswordHash;
|
||||||
|
|
||||||
|
// Save updated users
|
||||||
|
fs.writeFileSync(usersPath, JSON.stringify(users, null, 2));
|
||||||
|
|
||||||
|
console.log(`Password changed for user: ${username}`);
|
||||||
|
res.send(`✅ Password changed for user "${username}"`);
|
||||||
|
});
|
||||||
|
|
||||||
// 📚 Word list
|
// 📚 Word list
|
||||||
app.get('/getWordList', (req, res) => {
|
app.get('/getWordList', (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user