This commit is contained in:
@@ -47,7 +47,6 @@ const ALL_PERMISSIONS = [
|
||||
'Edit_Vehicle info_Repair Records',
|
||||
'Add_New Vehicle',
|
||||
'Archive_Vehicle',
|
||||
'Delete_Vehicle',
|
||||
'Export_Vehicle Report',
|
||||
'View_Transportation Schedule_Route Overview',
|
||||
'Create&Edit_Transportation Schedule',
|
||||
@@ -146,3 +145,30 @@ exports.login = (req, res) => {
|
||||
throw(Error('email or username is required'));
|
||||
}
|
||||
}
|
||||
|
||||
exports.me = (req, res) => {
|
||||
const condition = splitSite.splitSiteGet(req, { _id: req.userId });
|
||||
Employee.findOne(condition)
|
||||
.then((employee) => {
|
||||
if (!employee) {
|
||||
return res.status(404).send({ message: "User not found" });
|
||||
}
|
||||
if (!isEmployeeActive(employee)) {
|
||||
return res.status(403).send({ message: "User is not activated" });
|
||||
}
|
||||
return res.send({
|
||||
username: employee.username,
|
||||
email: employee.email,
|
||||
roles: employee.roles,
|
||||
permissions: getEffectivePermissions(employee),
|
||||
id: employee.id,
|
||||
name: employee.name,
|
||||
name_cn: employee.name_cn
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
res.status(500).send({
|
||||
message: err.message || "Failed to fetch current user"
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
module.exports = app => {
|
||||
const auth = require("../controllers/auth.controller");
|
||||
const { authJwt } = require("../middlewares");
|
||||
app.use(function(req, res, next) {
|
||||
res.header(
|
||||
"Access-Control-Allow-Headers",
|
||||
@@ -8,4 +9,5 @@ module.exports = app => {
|
||||
next();
|
||||
});
|
||||
app.post('/api/auth/login', auth.login);
|
||||
app.get('/api/auth/me', [authJwt.verifyToken], auth.me);
|
||||
};
|
||||
@@ -89,6 +89,7 @@ import { AuthService } from "./services";
|
||||
|
||||
function App() {
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [isPermissionReady, setIsPermissionReady] = useState(false);
|
||||
// const getLogo = () => {
|
||||
// return (window.location.hostname.includes('worldshine2.mayo.llc') || window.location.hostname.includes('site2') || window.location.host.includes('ws2') ||window.location.hostname.includes('localhost')) ? "/images/logo2.png" : ((window.location.hostname.includes('worldshine3.mayo.llc') ||window.location.hostname.includes('site3') || window.location.hostname.includes('ws3')) ? "/images/logo3.png" : "/images/logo1.png");
|
||||
// }
|
||||
@@ -158,6 +159,32 @@ function App() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const recheckPermissions = async () => {
|
||||
try {
|
||||
if (localStorage.getItem('token')) {
|
||||
await AuthService.refreshCurrentUserPermissions();
|
||||
}
|
||||
} catch (error) {
|
||||
// Token failures are handled by axios interceptor (logout + redirect).
|
||||
console.log(error);
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setIsPermissionReady(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
recheckPermissions();
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!isPermissionReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <div className="logo">
|
||||
|
||||
@@ -7,6 +7,7 @@ import moment from 'moment';
|
||||
|
||||
const InfoScreen = () => {
|
||||
const navigate = useNavigate();
|
||||
const canEditInfoScreen = AuthService.canEditInfoScreen();
|
||||
const canViewAppointmentCalendar = AuthService.hasPermission('View_Appointment Calendar');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [medicalEvents, setMedicalEvents] = useState([]);
|
||||
@@ -793,6 +794,7 @@ const InfoScreen = () => {
|
||||
<img src="/images/logo-trans.png" alt="Worldshine Logo" style={{ height: '30px', marginRight: '10px' }} />
|
||||
<strong className="logo-worldshine" style={{ color: '#0066B1', fontSize: '16px' }}>Worldshine</strong>
|
||||
</div>
|
||||
{canEditInfoScreen && (
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0"
|
||||
@@ -801,6 +803,7 @@ const InfoScreen = () => {
|
||||
>
|
||||
<PencilSquare size={16} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Layout */}
|
||||
@@ -848,6 +851,7 @@ const InfoScreen = () => {
|
||||
{/* Header with Title and Edit Button */}
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 className="text-black mb-0" style={{ fontSize: '14px', fontWeight: '500' }}>Gallery</h6>
|
||||
{canEditInfoScreen && (
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0"
|
||||
@@ -856,6 +860,7 @@ const InfoScreen = () => {
|
||||
>
|
||||
<PencilSquare size={16} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Image Carousel */}
|
||||
@@ -1173,9 +1178,11 @@ const InfoScreen = () => {
|
||||
|
||||
{!isFullScreen && (
|
||||
<div className="list-func-panel">
|
||||
{canEditInfoScreen && (
|
||||
<button className="btn btn-primary me-2" onClick={handleBackgroundSelect}>
|
||||
<FileImage size={16} className="me-2"></FileImage>Choose Background
|
||||
</button>
|
||||
)}
|
||||
<button className="btn btn-primary me-2" onClick={handleFullScreen}>
|
||||
<ArrowsFullscreen size={16} className="me-2"></ArrowsFullscreen>Full Screen
|
||||
</button>
|
||||
@@ -1185,7 +1192,7 @@ const InfoScreen = () => {
|
||||
</div>
|
||||
|
||||
{/* Edit AttendanceNote Modal */}
|
||||
<Modal show={showEditModal} onHide={handleCloseModal} size="lg">
|
||||
{canEditInfoScreen && <Modal show={showEditModal} onHide={handleCloseModal} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Edit Attendance Note</Modal.Title>
|
||||
</Modal.Header>
|
||||
@@ -1264,10 +1271,10 @@ const InfoScreen = () => {
|
||||
Save Changes
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</Modal>}
|
||||
|
||||
{/* Gallery Edit Modal */}
|
||||
<Modal show={showGalleryModal} onHide={handleGalleryClose} size="lg">
|
||||
{canEditInfoScreen && <Modal show={showGalleryModal} onHide={handleGalleryClose} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Update Images</Modal.Title>
|
||||
</Modal.Header>
|
||||
@@ -1333,10 +1340,10 @@ const InfoScreen = () => {
|
||||
Save Changes
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</Modal>}
|
||||
|
||||
{/* Background Selection Modal */}
|
||||
<Modal show={showBackgroundModal} onHide={handleBackgroundClose} size="md">
|
||||
{canEditInfoScreen && <Modal show={showBackgroundModal} onHide={handleBackgroundClose} size="md">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Choose Background Image</Modal.Title>
|
||||
</Modal.Header>
|
||||
@@ -1386,7 +1393,7 @@ const InfoScreen = () => {
|
||||
Save Background
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</Modal>}
|
||||
|
||||
{/* Full Screen Hint */}
|
||||
{isFullScreen && (
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
const MessageList = () => {
|
||||
const navigate = useNavigate();
|
||||
const canEditMessagingTemplate = AuthService.hasPermission('Create&Edit_Messaging Template');
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [activeTab, setActiveTab] = useState('allMessages');
|
||||
|
||||
@@ -90,6 +91,7 @@ const MessageList = () => {
|
||||
|
||||
// ---- Custom Template CRUD ----
|
||||
const openCreateTemplateModal = () => {
|
||||
if (!canEditMessagingTemplate) return;
|
||||
setEditingTemplate(null);
|
||||
setTemplateTitle('');
|
||||
setTemplateChinese('');
|
||||
@@ -98,6 +100,7 @@ const MessageList = () => {
|
||||
};
|
||||
|
||||
const openEditTemplateModal = (template) => {
|
||||
if (!canEditMessagingTemplate) return;
|
||||
setEditingTemplate(template);
|
||||
setTemplateTitle(template.title || '');
|
||||
setTemplateChinese(template.chinese || '');
|
||||
@@ -114,6 +117,7 @@ const MessageList = () => {
|
||||
};
|
||||
|
||||
const handleSaveTemplate = () => {
|
||||
if (!canEditMessagingTemplate) return;
|
||||
if (!templateTitle.trim()) { window.alert('Template Title is required.'); return; }
|
||||
if (!templateChinese.trim()) { window.alert('Message Content (Chinese) is required.'); return; }
|
||||
if (!templateEnglish.trim()) { window.alert('Message Content (English) is required.'); return; }
|
||||
@@ -131,6 +135,7 @@ const MessageList = () => {
|
||||
};
|
||||
|
||||
const handleDeleteTemplate = (id) => {
|
||||
if (!canEditMessagingTemplate) return;
|
||||
if (window.confirm('Are you sure you want to delete this template?')) {
|
||||
MessageService.deleteCustomTemplate(id).then(() => { fetchCustomTemplates(); });
|
||||
}
|
||||
@@ -399,7 +404,7 @@ const MessageList = () => {
|
||||
<th>Template Title</th>
|
||||
<th>Message Content (Chinese)</th>
|
||||
<th>Message Content (English)</th>
|
||||
<th style={{ width: '80px' }}>Actions</th>
|
||||
{canEditMessagingTemplate && <th style={{ width: '80px' }}>Actions</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -409,14 +414,14 @@ const MessageList = () => {
|
||||
<td>{template?.title}</td>
|
||||
<td style={{ maxWidth: '300px', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{template?.chinese}</td>
|
||||
<td style={{ maxWidth: '300px', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{template?.english}</td>
|
||||
<td>
|
||||
{canEditMessagingTemplate && <td>
|
||||
<PencilSquare size={16} className="clickable me-2" onClick={() => openEditTemplateModal(template)} />
|
||||
<Trash size={16} className="clickable" color="#dc3545" onClick={() => handleDeleteTemplate(template.id)} />
|
||||
</td>
|
||||
</td>}
|
||||
</tr>
|
||||
))}
|
||||
{customTemplates.length === 0 && (
|
||||
<tr><td colSpan="5" style={{ textAlign: 'center', color: '#999', padding: '24px' }}>No templates yet. Create one to get started.</td></tr>
|
||||
<tr><td colSpan={canEditMessagingTemplate ? 5 : 4} style={{ textAlign: 'center', color: '#999', padding: '24px' }}>No templates yet. Create one to get started.</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -482,14 +487,14 @@ const MessageList = () => {
|
||||
</>
|
||||
)}
|
||||
{activeTab === 'messageTemplate' && (
|
||||
<button className="btn btn-primary btn-sm" onClick={openCreateTemplateModal}><Plus size={16} className="me-1" />Create Message Template</button>
|
||||
canEditMessagingTemplate && <button className="btn btn-primary btn-sm" onClick={openCreateTemplateModal}><Plus size={16} className="me-1" />Create Message Template</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create / Edit Message Template Modal */}
|
||||
<Modal show={showCreateTemplateModal} onHide={closeTemplateModal} centered>
|
||||
<Modal show={canEditMessagingTemplate && showCreateTemplateModal} onHide={closeTemplateModal} centered>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{editingTemplate ? 'Edit Message Template' : 'Create New Message Template'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
const UpdateVehicle = () => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const canArchiveVehicle = AuthService.canArchiveVehicle();
|
||||
const params = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const validTabs = ['basicInfo', 'complianceDeadlines'];
|
||||
@@ -209,6 +210,7 @@ const UpdateVehicle = () => {
|
||||
}
|
||||
|
||||
const triggerShowDeleteModal = () => {
|
||||
if (!canArchiveVehicle) return;
|
||||
setShowDeleteModal(true);
|
||||
}
|
||||
|
||||
@@ -217,6 +219,7 @@ const UpdateVehicle = () => {
|
||||
}
|
||||
|
||||
const deactivateVehicle = () => {
|
||||
if (!canArchiveVehicle) return;
|
||||
const data = buildVehicleData();
|
||||
data.status = 'inactive';
|
||||
dispatch(deleteVehicle({id: params.id, data}));
|
||||
@@ -451,7 +454,7 @@ const UpdateVehicle = () => {
|
||||
|
||||
</Tabs>
|
||||
<div className="list-func-panel">
|
||||
<button className="btn btn-primary" onClick={() => triggerShowDeleteModal()}><Archive size={16} className="me-2"></Archive>Archive</button>
|
||||
{canArchiveVehicle && <button className="btn btn-primary" onClick={() => triggerShowDeleteModal()}><Archive size={16} className="me-2"></Archive>Archive</button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,20 @@ const login = (data) => {
|
||||
return http.post('/auth/login', data);
|
||||
};
|
||||
|
||||
const getCurrentUserFromServer = () => {
|
||||
return http.get('/auth/me');
|
||||
};
|
||||
|
||||
const refreshCurrentUserPermissions = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return null;
|
||||
const { data } = await getCurrentUserFromServer();
|
||||
if (data) {
|
||||
localStorage.setItem('user', JSON.stringify(data));
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const logout = (data, shouldClearSession = false) => {
|
||||
if (shouldClearSession) {
|
||||
localStorage.removeItem('user');
|
||||
@@ -162,10 +176,9 @@ const canViewVehicleRepairs = () => hasAnyPermission(['View_Vehicle info_Repair
|
||||
const canEditVehicleRepairs = () => hasPermission('Edit_Vehicle info_Repair Records');
|
||||
const canAddVehicle = () => hasPermission('Add_New Vehicle');
|
||||
const canArchiveVehicle = () => hasPermission('Archive_Vehicle');
|
||||
const canDeleteVehicle = () => hasPermission('Delete_Vehicle');
|
||||
const canExportVehicleReport = () => hasPermission('Export_Vehicle Report');
|
||||
const canViewVehicleAny = () => canViewVehicleBasic() || canViewVehicleDocuments() || canViewVehicleRepairs() || canEditVehicleBasic() || canEditVehicleDocuments() || canEditVehicleRepairs() || canAddVehicle() || canArchiveVehicle() || canDeleteVehicle() || canExportVehicleReport();
|
||||
const canEditVehicleAny = () => canEditVehicleBasic() || canEditVehicleDocuments() || canEditVehicleRepairs() || canAddVehicle() || canArchiveVehicle() || canDeleteVehicle();
|
||||
const canViewVehicleAny = () => canViewVehicleBasic() || canViewVehicleDocuments() || canViewVehicleRepairs() || canEditVehicleBasic() || canEditVehicleDocuments() || canEditVehicleRepairs() || canAddVehicle() || canArchiveVehicle() || canExportVehicleReport();
|
||||
const canEditVehicleAny = () => canEditVehicleBasic() || canEditVehicleDocuments() || canEditVehicleRepairs() || canAddVehicle() || canArchiveVehicle();
|
||||
|
||||
const canViewDashboard = () => {
|
||||
return hasPermission('Dashboard');
|
||||
@@ -178,6 +191,10 @@ const canViewInfoScreen = () => {
|
||||
]);
|
||||
}
|
||||
|
||||
const canEditInfoScreen = () => {
|
||||
return hasPermission('Edit_Info Screen');
|
||||
}
|
||||
|
||||
const canViewCalendar = () => {
|
||||
return canViewAnyCalendarTab() || canEditAnyCalendarTab();
|
||||
}
|
||||
@@ -395,10 +412,13 @@ const getLocalAccessToken = () => {
|
||||
|
||||
export const AuthService = {
|
||||
login,
|
||||
getCurrentUserFromServer,
|
||||
refreshCurrentUserPermissions,
|
||||
logout,
|
||||
hasPermission,
|
||||
canViewDashboard,
|
||||
canViewInfoScreen,
|
||||
canEditInfoScreen,
|
||||
canViewCalendarTab,
|
||||
canEditCalendarTab,
|
||||
canViewAnyCalendarTab,
|
||||
@@ -443,7 +463,6 @@ export const AuthService = {
|
||||
canEditVehicleRepairs,
|
||||
canAddVehicle,
|
||||
canArchiveVehicle,
|
||||
canDeleteVehicle,
|
||||
canExportVehicleReport,
|
||||
canViewVehicleAny,
|
||||
canEditVehicleAny,
|
||||
|
||||
@@ -180,7 +180,6 @@ export const EMPLOYEE_PERMISSION_GROUPS = {
|
||||
'Edit_Vehicle info_Repair Records',
|
||||
'Add_New Vehicle',
|
||||
'Archive_Vehicle',
|
||||
'Delete_Vehicle',
|
||||
'Export_Vehicle Report'
|
||||
],
|
||||
Transportation: [
|
||||
|
||||
Reference in New Issue
Block a user