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
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