feat: Add customer password change functionality in admin panel

This commit is contained in:
oguz ozturk 2026-01-12 18:54:39 +03:00
parent 9cba460d4d
commit 50a7717c15
3 changed files with 179 additions and 0 deletions

View File

@ -191,6 +191,28 @@ def update_customer_status(current_admin, customer_id):
return jsonify({'error': str(e)}), 500
@customers_bp.route('/<int:customer_id>/password', methods=['PUT'])
@token_required
def change_customer_password(current_admin, customer_id):
"""Change customer password"""
try:
data = request.get_json()
result = call_customer_api(
f'/api/admin/customers/{customer_id}/password',
method='PUT',
data=data
)
if result and result.get('status') == 'success':
return jsonify(result), 200
else:
return jsonify(result or {'error': 'Failed to change password'}), 500
except Exception as e:
return jsonify({'error': str(e)}), 500
@customers_bp.route('/stats', methods=['GET'])
@token_required
def get_customer_stats(current_admin):

View File

@ -14,6 +14,7 @@ const Customers = () => {
const [showPlanModal, setShowPlanModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showPasswordModal, setShowPasswordModal] = useState(false);
const [showToast, setShowToast] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const [toastType, setToastType] = useState('success'); // success, error
@ -121,6 +122,18 @@ const Customers = () => {
}
};
const handleChangePassword = async (customerId, passwordData) => {
try {
await customerService.changeCustomerPassword(customerId, passwordData);
setShowPasswordModal(false);
setSelectedCustomer(null);
showSuccessToast('Password changed successfully!');
} catch (err) {
console.error('Failed to change password:', err);
showErrorToast('Failed to change password: ' + (err.response?.data?.message || err.message));
}
};
const getStatusBadge = (isActive, subscriptionStatus) => {
if (!isActive) {
return <span className="badge badge-danger">Inactive</span>;
@ -256,6 +269,16 @@ const Customers = () => {
>
📦
</button>
<button
onClick={() => {
setSelectedCustomer(customer);
setShowPasswordModal(true);
}}
className="text-indigo-600 hover:text-indigo-800"
title="Change Password"
>
🔑
</button>
{customer.subscription_status === 'suspended' || !customer.is_active ? (
<button
onClick={() => handleActivateCustomer(customer.id)}
@ -329,6 +352,18 @@ const Customers = () => {
/>
)}
{/* Password Change Modal */}
{showPasswordModal && selectedCustomer && (
<PasswordChangeModal
customer={selectedCustomer}
onClose={() => {
setShowPasswordModal(false);
setSelectedCustomer(null);
}}
onUpdate={handleChangePassword}
/>
)}
{/* Toast Notification */}
{showToast && (
<div className={`fixed bottom-6 right-6 px-6 py-4 rounded-lg shadow-lg transform transition-all duration-300 z-50 ${
@ -631,5 +666,120 @@ const DeleteConfirmModal = ({ customer, onClose, onConfirm }) => {
);
};
// Password Change Modal Component
const PasswordChangeModal = ({ customer, onClose, onUpdate }) => {
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
// Validation
if (!newPassword) {
setError('Password is required');
return;
}
if (newPassword.length < 8) {
setError('Password must be at least 8 characters');
return;
}
if (newPassword !== confirmPassword) {
setError('Passwords do not match');
return;
}
setLoading(true);
try {
await onUpdate(customer.id, { new_password: newPassword });
} catch (err) {
setError(err.response?.data?.message || 'Failed to change password');
} finally {
setLoading(false);
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-900">Change Password</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 text-2xl"
>
×
</button>
</div>
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded">
<p className="text-sm text-blue-800">
<strong>Customer:</strong> {customer.full_name} ({customer.email})
</p>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded">
<p className="text-sm text-red-800">{error}</p>
</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
New Password
</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="input w-full"
placeholder="Enter new password (min 8 characters)"
disabled={loading}
autoFocus
/>
</div>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Confirm Password
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="input w-full"
placeholder="Confirm new password"
disabled={loading}
/>
</div>
<div className="flex gap-3">
<button
type="submit"
className="flex-1 btn-primary"
disabled={loading}
>
{loading ? 'Changing...' : 'Change Password'}
</button>
<button
type="button"
onClick={onClose}
className="flex-1 btn-outline"
disabled={loading}
>
Cancel
</button>
</div>
</form>
</div>
</div>
);
};
export default Customers;

View File

@ -58,6 +58,12 @@ export const getCustomerStats = async () => {
return response.data;
};
// Change customer password
export const changeCustomerPassword = async (customerId, passwordData) => {
const response = await api.put(`/api/customers/${customerId}/password`, passwordData);
return response.data;
};
export default {
getCustomers,
getCustomer,
@ -68,5 +74,6 @@ export default {
updateCustomerPlan,
updateCustomerStatus,
getCustomerStats,
changeCustomerPassword,
};