feat: add change password functionality for users
Add ChangePasswordModal component: - Create modal with new password and confirmation fields - Implement password validation (min 8 chars, uppercase, lowercase, number, special char) - Add real-time error validation and display - Show password requirements info box - Display username for confirmation - Add loading state during password change - Success/error notifications Update IndexUser: - Add state management for change password modal - Pass props to ListUser and ChangePasswordModal - Integrate ChangePasswordModal component Update ListUser: - Add KeyOutlined icon for change password button - Add purple change password button in action column - Implement showChangePasswordModal function - Update columns to include change password handler - Increase action column width to 18% API endpoint: PUT /api/user/change-password/:id
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { memo, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ListUser from './component/ListUser';
|
||||
import DetailUser from './component/DetailUser';
|
||||
import ChangePasswordModal from './component/ChangePasswordModal';
|
||||
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
|
||||
import { Typography } from 'antd';
|
||||
|
||||
@@ -15,6 +16,8 @@ const IndexUser = memo(function IndexUser() {
|
||||
const [selectedData, setSelectedData] = useState(null);
|
||||
const [readOnly, setReadOnly] = useState(false);
|
||||
const [showModal, setShowmodal] = useState(false);
|
||||
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
|
||||
const [selectedUserForPassword, setSelectedUserForPassword] = useState(null);
|
||||
|
||||
const setMode = (param) => {
|
||||
setShowmodal(true);
|
||||
@@ -63,6 +66,8 @@ const IndexUser = memo(function IndexUser() {
|
||||
selectedData={selectedData}
|
||||
setSelectedData={setSelectedData}
|
||||
readOnly={readOnly}
|
||||
setShowChangePasswordModal={setShowChangePasswordModal}
|
||||
setSelectedUserForPassword={setSelectedUserForPassword}
|
||||
/>
|
||||
<DetailUser
|
||||
setActionMode={setMode}
|
||||
@@ -72,6 +77,12 @@ const IndexUser = memo(function IndexUser() {
|
||||
showModal={showModal}
|
||||
actionMode={actionMode}
|
||||
/>
|
||||
<ChangePasswordModal
|
||||
showModal={showChangePasswordModal}
|
||||
setShowModal={setShowChangePasswordModal}
|
||||
selectedUser={selectedUserForPassword}
|
||||
setSelectedUser={setSelectedUserForPassword}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
258
src/pages/user/component/ChangePasswordModal.jsx
Normal file
258
src/pages/user/component/ChangePasswordModal.jsx
Normal file
@@ -0,0 +1,258 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Input, Typography, Button, ConfigProvider } from 'antd';
|
||||
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
|
||||
import { changePassword } from '../../../api/user';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const ChangePasswordModal = (props) => {
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const validatePassword = (password) => {
|
||||
if (!password) return 'Password wajib diisi';
|
||||
|
||||
// Must be at least 8 characters long
|
||||
if (password.length < 8) {
|
||||
return 'Password must be at least 8 characters long';
|
||||
}
|
||||
|
||||
// Must contain at least one uppercase letter
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
return 'Password must contain at least one uppercase letter';
|
||||
}
|
||||
|
||||
// Must contain at least one lowercase letter
|
||||
if (!/[a-z]/.test(password)) {
|
||||
return 'Password must contain at least one lowercase letter';
|
||||
}
|
||||
|
||||
// Must contain at least one number
|
||||
if (!/\d/.test(password)) {
|
||||
return 'Password must contain at least one number';
|
||||
}
|
||||
|
||||
// Must contain at least one special character
|
||||
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
||||
return 'Password must contain at least one special character';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
|
||||
const passwordError = validatePassword(formData.newPassword);
|
||||
if (passwordError) {
|
||||
newErrors.newPassword = passwordError;
|
||||
}
|
||||
|
||||
if (!formData.confirmPassword) {
|
||||
newErrors.confirmPassword = 'Konfirmasi password wajib diisi';
|
||||
} else if (formData.newPassword !== formData.confirmPassword) {
|
||||
newErrors.confirmPassword = 'Password tidak cocok';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
props.setShowModal(false);
|
||||
props.setSelectedUser(null);
|
||||
setFormData({
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
setErrors({});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateForm()) {
|
||||
NotifOk({
|
||||
icon: 'warning',
|
||||
title: 'Peringatan',
|
||||
message: 'Mohon periksa kembali form Anda',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setConfirmLoading(true);
|
||||
|
||||
try {
|
||||
const response = await changePassword(props.selectedUser.user_id, formData.newPassword);
|
||||
|
||||
console.log('Change Password Response:', response);
|
||||
|
||||
if (response && response.statusCode === 200) {
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: `Password untuk user "${props.selectedUser.user_fullname}" berhasil diubah.`,
|
||||
});
|
||||
|
||||
handleCancel();
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
message: response?.message || 'Terjadi kesalahan saat mengubah password.',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Change Password Error:', error);
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: error.message || 'Terjadi kesalahan pada server. Coba lagi nanti.',
|
||||
});
|
||||
}
|
||||
|
||||
setConfirmLoading(false);
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value,
|
||||
});
|
||||
// Clear error for this field
|
||||
if (errors[name]) {
|
||||
setErrors({
|
||||
...errors,
|
||||
[name]: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.showModal) {
|
||||
setFormData({
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
setErrors({});
|
||||
}
|
||||
}, [props.showModal]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`Ubah Password - ${props.selectedUser?.user_fullname || ''}`}
|
||||
open={props.showModal}
|
||||
onCancel={handleCancel}
|
||||
footer={[
|
||||
<React.Fragment key="modal-footer">
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: { colorBgContainer: '#E9F6EF' },
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: 'white',
|
||||
defaultColor: '#23A55A',
|
||||
defaultBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
defaultHoverBorderColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button onClick={handleCancel}>Batal</Button>
|
||||
</ConfigProvider>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorBgContainer: '#209652',
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: '#23a55a',
|
||||
defaultColor: '#FFFFFF',
|
||||
defaultBorderColor: '#23a55a',
|
||||
defaultHoverColor: '#FFFFFF',
|
||||
defaultHoverBorderColor: '#23a55a',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button loading={confirmLoading} onClick={handleSave}>
|
||||
Simpan
|
||||
</Button>
|
||||
</ConfigProvider>
|
||||
</React.Fragment>,
|
||||
]}
|
||||
width={500}
|
||||
>
|
||||
<div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Username</Text>
|
||||
<Input
|
||||
value={props.selectedUser?.user_name || ''}
|
||||
disabled
|
||||
style={{ backgroundColor: '#f5f5f5' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Password Baru</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Input.Password
|
||||
name="newPassword"
|
||||
value={formData.newPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Masukkan password baru"
|
||||
status={errors.newPassword ? 'error' : ''}
|
||||
/>
|
||||
{errors.newPassword && (
|
||||
<Text style={{ color: 'red', fontSize: '12px' }}>{errors.newPassword}</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Konfirmasi Password</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Input.Password
|
||||
name="confirmPassword"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Konfirmasi password baru"
|
||||
status={errors.confirmPassword ? 'error' : ''}
|
||||
/>
|
||||
{errors.confirmPassword && (
|
||||
<Text style={{ color: 'red', fontSize: '12px' }}>
|
||||
{errors.confirmPassword}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
padding: 12,
|
||||
backgroundColor: '#f0f8ff',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #d6e4ff',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: '12px', color: '#595959' }}>
|
||||
<strong>Persyaratan password:</strong>
|
||||
<ul style={{ marginTop: 8, marginBottom: 0, paddingLeft: 20 }}>
|
||||
<li>Minimal 8 karakter</li>
|
||||
<li>Minimal 1 huruf besar (A-Z)</li>
|
||||
<li>Minimal 1 huruf kecil (a-z)</li>
|
||||
<li>Minimal 1 angka (0-9)</li>
|
||||
<li>Minimal 1 karakter spesial (!@#$%^&*)</li>
|
||||
</ul>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePasswordModal;
|
||||
Reference in New Issue
Block a user