Password updator for users / admin

This commit is contained in:
admin 2025-10-15 10:51:08 -06:00
parent 06ecffaf4d
commit 4fb9847812
5 changed files with 436 additions and 0 deletions

View File

@ -188,6 +188,77 @@ class AuthController {
}); });
} }
} }
async changePassword(req, res) {
try {
const { currentPassword, newPassword } = req.body;
// Validate input
if (!currentPassword || !newPassword) {
return res.status(400).json({
success: false,
error: 'Current password and new password are required'
});
}
// Validate new password strength
if (newPassword.length < 8) {
return res.status(400).json({
success: false,
error: 'New password must be at least 8 characters long'
});
}
// Get user from session
const userId = req.session.userId;
const userEmail = req.session.userEmail;
if (!userId || !userEmail) {
return res.status(401).json({
success: false,
error: 'Session expired. Please login again.'
});
}
// Fetch user from NocoDB to verify current password
const user = await nocodbService.getUserByEmail(userEmail);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
// Verify current password
const storedPassword = user.Password || user.password;
if (storedPassword !== currentPassword) {
return res.status(401).json({
success: false,
error: 'Current password is incorrect'
});
}
// Update password in NocoDB
await nocodbService.updateUser(userId, {
Password: newPassword
});
console.log('Password changed successfully for user:', userEmail);
res.json({
success: true,
message: 'Password changed successfully'
});
} catch (error) {
console.error('Change password error:', error);
res.status(500).json({
success: false,
error: 'Failed to change password. Please try again later.'
});
}
}
} }
module.exports = new AuthController(); module.exports = new AuthController();

View File

@ -254,9 +254,13 @@
background: #d4edda; background: #d4edda;
color: #155724; color: #155724;
border: 1px solid #c3e6cb; border: 1px solid #c3e6cb;
border-left: 4px solid #27ae60;
padding: 1rem; padding: 1rem;
border-radius: 4px; border-radius: 4px;
margin-bottom: 1rem; margin-bottom: 1rem;
display: flex;
align-items: center;
font-weight: 500;
} }
.message-error { .message-error {
@ -268,6 +272,114 @@
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.password-success-animation {
animation: successPulse 0.6s ease-out;
}
@keyframes successPulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(39, 174, 96, 0.7);
}
50% {
transform: scale(1.02);
box-shadow: 0 0 0 10px rgba(39, 174, 96, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(39, 174, 96, 0);
}
}
.success-checkmark {
display: inline-block;
width: 24px;
height: 24px;
border-radius: 50%;
background: #27ae60;
color: white;
text-align: center;
line-height: 24px;
margin-right: 0.5rem;
font-weight: bold;
animation: checkmarkPop 0.4s ease-out;
}
@keyframes checkmarkPop {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
opacity: 1;
}
}
/* Toast notification styles */
.toast-notification {
position: fixed;
top: 20px;
right: 20px;
background: #27ae60;
color: white;
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 10000;
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 500;
animation: toastSlideIn 0.4s ease-out;
max-width: 400px;
}
.toast-notification.error {
background: #e74c3c;
}
.toast-notification .toast-icon {
font-size: 1.5rem;
width: 32px;
height: 32px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
@keyframes toastSlideIn {
0% {
transform: translateX(400px);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
@keyframes toastSlideOut {
0% {
transform: translateX(0);
opacity: 1;
}
100% {
transform: translateX(400px);
opacity: 0;
}
}
.toast-notification.toast-exit {
animation: toastSlideOut 0.4s ease-in forwards;
}
.stats-grid { .stats-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
@ -964,6 +1076,7 @@ Sincerely,
<h2>Account Settings</h2> <h2>Account Settings</h2>
<div class="account-info"> <div class="account-info">
<h3 style="color: #2c3e50; margin-bottom: 1.5rem;">Account Information</h3>
<form id="account-form"> <form id="account-form">
<div class="form-group"> <div class="form-group">
<label for="account-name">Full Name</label> <label for="account-name">Full Name</label>
@ -991,6 +1104,40 @@ Sincerely,
To update your account information, please contact an administrator. To update your account information, please contact an administrator.
</p> </p>
</div> </div>
<!-- Password Change Section -->
<div class="account-info" style="margin-top: 2rem;">
<h3 style="color: #2c3e50; margin-bottom: 1rem;">Change Password</h3>
<p style="color: #666; font-size: 0.9rem; margin-bottom: 1.5rem;">
For security reasons, please use a strong password with at least 8 characters.
</p>
<div id="password-message" class="hidden"></div>
<form id="change-password-form">
<div class="form-group">
<label for="current-password">Current Password *</label>
<input type="password" id="current-password" name="currentPassword" required
placeholder="Enter your current password">
</div>
<div class="form-group">
<label for="new-password">New Password *</label>
<input type="password" id="new-password" name="newPassword" required
placeholder="Enter your new password (min 8 characters)"
minlength="8">
</div>
<div class="form-group">
<label for="confirm-password">Confirm New Password *</label>
<input type="password" id="confirm-password" name="confirmPassword" required
placeholder="Re-enter your new password"
minlength="8">
</div>
<div class="form-row">
<button type="submit" class="btn btn-primary">Change Password</button>
<button type="button" class="btn btn-secondary" id="cancel-password-change">Cancel</button>
</div>
</form>
</div>
</div> </div>
</div> </div>

View File

@ -193,6 +193,34 @@ class AuthManager {
return true; return true;
} }
// Change user password
async changePassword(currentPassword, newPassword) {
try {
const response = await apiClient.post('/auth/change-password', {
currentPassword,
newPassword
});
if (response.success) {
return {
success: true,
message: response.message || 'Password changed successfully'
};
} else {
return {
success: false,
error: response.error || 'Failed to change password'
};
}
} catch (error) {
console.error('Change password failed:', error);
return {
success: false,
error: error.message || 'Failed to change password. Please try again.'
};
}
}
} }
// Create global auth manager instance // Create global auth manager instance

View File

@ -124,6 +124,22 @@ class UserDashboard {
}); });
} }
// Password change form
const passwordForm = document.getElementById('change-password-form');
if (passwordForm) {
passwordForm.addEventListener('submit', (e) => {
this.handlePasswordChange(e);
});
}
// Cancel password change button
const cancelPasswordBtn = document.getElementById('cancel-password-change');
if (cancelPasswordBtn) {
cancelPasswordBtn.addEventListener('click', () => {
this.resetPasswordForm();
});
}
// Response filter changes // Response filter changes
const responseCampaignFilter = document.getElementById('responses-campaign-filter'); const responseCampaignFilter = document.getElementById('responses-campaign-filter');
const responseStatusFilter = document.getElementById('responses-status-filter'); const responseStatusFilter = document.getElementById('responses-status-filter');
@ -1356,6 +1372,176 @@ class UserDashboard {
this.showMessage('Failed to create campaign: ' + error.message, 'error'); this.showMessage('Failed to create campaign: ' + error.message, 'error');
} }
} }
// Password Change Methods
async handlePasswordChange(e) {
e.preventDefault();
const currentPassword = document.getElementById('current-password').value;
const newPassword = document.getElementById('new-password').value;
const confirmPassword = document.getElementById('confirm-password').value;
// Client-side validation
if (newPassword !== confirmPassword) {
this.showPasswordMessage('New passwords do not match', 'error');
return;
}
if (newPassword.length < 8) {
this.showPasswordMessage('Password must be at least 8 characters long', 'error');
return;
}
if (currentPassword === newPassword) {
this.showPasswordMessage('New password must be different from current password', 'error');
return;
}
try {
// Disable submit button
const submitBtn = e.target.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.textContent = 'Changing Password...';
const result = await this.authManager.changePassword(currentPassword, newPassword);
// Re-enable button
submitBtn.disabled = false;
submitBtn.textContent = originalText;
if (result.success) {
this.showPasswordMessage(result.message || 'Password changed successfully!', 'success');
// Show toast notification
this.showToast('🎉 Password changed successfully! Your account is now more secure.', 'success');
// Visual feedback: briefly change button to success state
submitBtn.style.transition = 'all 0.3s ease';
submitBtn.style.backgroundColor = '#27ae60';
submitBtn.textContent = '✓ Password Changed!';
// Reset button after 2 seconds, then clear form
setTimeout(() => {
submitBtn.style.backgroundColor = '';
submitBtn.textContent = originalText;
this.resetPasswordForm();
}, 2000);
} else {
this.showPasswordMessage(result.error || 'Failed to change password', 'error');
}
} catch (error) {
console.error('Password change error:', error);
this.showPasswordMessage('Failed to change password: ' + error.message, 'error');
// Re-enable button on error
const submitBtn = e.target.querySelector('button[type="submit"]');
submitBtn.disabled = false;
submitBtn.textContent = 'Change Password';
}
}
resetPasswordForm() {
const form = document.getElementById('change-password-form');
if (form) {
form.reset();
}
// Clear any messages
const messageContainer = document.getElementById('password-message');
if (messageContainer) {
messageContainer.textContent = '';
messageContainer.className = 'hidden';
}
}
showPasswordMessage(message, type) {
const messageContainer = document.getElementById('password-message');
if (messageContainer) {
// Clear previous content
messageContainer.innerHTML = '';
if (type === 'success') {
// Add checkmark icon for success
const checkmark = document.createElement('span');
checkmark.className = 'success-checkmark';
checkmark.textContent = '✓';
messageContainer.appendChild(checkmark);
// Add message text
const messageText = document.createElement('span');
messageText.textContent = message;
messageContainer.appendChild(messageText);
messageContainer.className = 'message-success password-success-animation';
// Add animation to the password form section
const passwordSection = document.getElementById('change-password-form').parentElement;
if (passwordSection) {
passwordSection.style.transition = 'all 0.3s ease';
passwordSection.style.borderColor = '#27ae60';
passwordSection.style.boxShadow = '0 0 20px rgba(39, 174, 96, 0.3)';
// Reset the border after animation
setTimeout(() => {
passwordSection.style.borderColor = '';
passwordSection.style.boxShadow = '';
}, 2000);
}
} else {
// Error message without checkmark
messageContainer.textContent = message;
messageContainer.className = 'message-error';
}
}
// Auto-hide success messages after 5 seconds
if (type === 'success') {
setTimeout(() => {
if (messageContainer) {
messageContainer.style.transition = 'opacity 0.5s ease';
messageContainer.style.opacity = '0';
setTimeout(() => {
messageContainer.className = 'hidden';
messageContainer.style.opacity = '1';
}, 500);
}
}, 5000);
}
}
showToast(message, type = 'success') {
// Create toast element
const toast = document.createElement('div');
toast.className = `toast-notification ${type === 'error' ? 'error' : ''}`;
// Create icon
const icon = document.createElement('div');
icon.className = 'toast-icon';
icon.textContent = type === 'success' ? '✓' : '✕';
// Create message text
const messageText = document.createElement('span');
messageText.textContent = message;
// Assemble toast
toast.appendChild(icon);
toast.appendChild(messageText);
// Add to page
document.body.appendChild(toast);
// Auto-remove after 4 seconds with animation
setTimeout(() => {
toast.classList.add('toast-exit');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 400); // Match animation duration
}, 4000);
}
} }
// Initialize dashboard when DOM is loaded // Initialize dashboard when DOM is loaded

View File

@ -1,5 +1,6 @@
const express = require('express'); const express = require('express');
const authController = require('../controllers/authController'); const authController = require('../controllers/authController');
const { requireAuth } = require('../middleware/auth');
const router = express.Router(); const router = express.Router();
@ -12,4 +13,7 @@ router.post('/logout', authController.logout);
// GET /api/auth/session // GET /api/auth/session
router.get('/session', authController.checkSession); router.get('/session', authController.checkSession);
// POST /api/auth/change-password (requires authentication)
router.post('/change-password', requireAuth, authController.changePassword);
module.exports = router; module.exports = router;