Password updator for users / admin
This commit is contained in:
parent
06ecffaf4d
commit
4fb9847812
@ -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();
|
||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
Loading…
x
Reference in New Issue
Block a user