diff --git a/influence/app/controllers/authController.js b/influence/app/controllers/authController.js index a9283f7..0fc45ed 100644 --- a/influence/app/controllers/authController.js +++ b/influence/app/controllers/authController.js @@ -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(); \ No newline at end of file diff --git a/influence/app/public/dashboard.html b/influence/app/public/dashboard.html index ef93063..5039558 100644 --- a/influence/app/public/dashboard.html +++ b/influence/app/public/dashboard.html @@ -254,9 +254,13 @@ background: #d4edda; color: #155724; border: 1px solid #c3e6cb; + border-left: 4px solid #27ae60; padding: 1rem; border-radius: 4px; margin-bottom: 1rem; + display: flex; + align-items: center; + font-weight: 500; } .message-error { @@ -268,6 +272,114 @@ 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 { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); @@ -964,6 +1076,7 @@ Sincerely,

Account Settings

+

Account Information

@@ -991,6 +1104,40 @@ Sincerely, To update your account information, please contact an administrator.

+ + +
diff --git a/influence/app/public/js/auth.js b/influence/app/public/js/auth.js index 324f3c9..6962437 100644 --- a/influence/app/public/js/auth.js +++ b/influence/app/public/js/auth.js @@ -193,6 +193,34 @@ class AuthManager { 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 diff --git a/influence/app/public/js/dashboard.js b/influence/app/public/js/dashboard.js index 42429db..137c36a 100644 --- a/influence/app/public/js/dashboard.js +++ b/influence/app/public/js/dashboard.js @@ -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 const responseCampaignFilter = document.getElementById('responses-campaign-filter'); const responseStatusFilter = document.getElementById('responses-status-filter'); @@ -1356,6 +1372,176 @@ class UserDashboard { 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 diff --git a/influence/app/routes/auth.js b/influence/app/routes/auth.js index e0e320d..0827d0f 100644 --- a/influence/app/routes/auth.js +++ b/influence/app/routes/auth.js @@ -1,5 +1,6 @@ const express = require('express'); const authController = require('../controllers/authController'); +const { requireAuth } = require('../middleware/auth'); const router = express.Router(); @@ -12,4 +13,7 @@ router.post('/logout', authController.logout); // GET /api/auth/session router.get('/session', authController.checkSession); +// POST /api/auth/change-password (requires authentication) +router.post('/change-password', requireAuth, authController.changePassword); + module.exports = router; \ No newline at end of file