feat: Enhance admin access and UI for privileged users

Grant 'poweruser' role access to administrative settings and sections. Update UI to reflect elevated privileges and adjust default tab navigation for these users. Modify server-side access control to include 'poweruser' alongside 'admin' for privileged routes.
This commit is contained in:
2025-12-07 23:30:06 +01:00
parent 80d658a536
commit 0f82df517b
2 changed files with 97 additions and 42 deletions

View File

@@ -6,12 +6,13 @@ import { Save, Building, Coins, Plus, Pencil, Trash2, X, CalendarCheck, AlertTri
export const SettingsPage: React.FC = () => { export const SettingsPage: React.FC = () => {
const currentUser = CondoService.getCurrentUser(); const currentUser = CondoService.getCurrentUser();
const isAdmin = currentUser?.role === 'admin'; const isSuperAdmin = currentUser?.role === 'admin';
const isPrivileged = currentUser?.role === 'admin' || currentUser?.role === 'poweruser';
// Tab configuration // Tab configuration
type TabType = 'profile' | 'features' | 'general' | 'condos' | 'families' | 'users' | 'notices' | 'alerts' | 'smtp'; type TabType = 'profile' | 'features' | 'general' | 'condos' | 'families' | 'users' | 'notices' | 'alerts';
const [activeTab, setActiveTab] = useState<TabType>(isAdmin ? 'general' : 'profile'); const [activeTab, setActiveTab] = useState<TabType>(isPrivileged ? 'general' : 'profile');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// Profile State // Profile State
@@ -88,6 +89,9 @@ export const SettingsPage: React.FC = () => {
active: true active: true
}); });
// SMTP Modal State
const [showSmtpModal, setShowSmtpModal] = useState(false);
// Notices (Bacheca) State // Notices (Bacheca) State
const [notices, setNotices] = useState<Notice[]>([]); const [notices, setNotices] = useState<Notice[]>([]);
const [showNoticeModal, setShowNoticeModal] = useState(false); const [showNoticeModal, setShowNoticeModal] = useState(false);
@@ -116,7 +120,7 @@ export const SettingsPage: React.FC = () => {
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { try {
if (isAdmin) { if (isPrivileged) {
// First fetch global/structural data // First fetch global/structural data
const condoList = await CondoService.getCondos(); const condoList = await CondoService.getCondos();
const activeC = await CondoService.getActiveCondo(); const activeC = await CondoService.getActiveCondo();
@@ -165,7 +169,7 @@ export const SettingsPage: React.FC = () => {
} }
}; };
fetchData(); fetchData();
}, [isAdmin]); }, [isPrivileged]);
// --- Profile Handlers --- // --- Profile Handlers ---
const handleProfileSubmit = async (e: React.FormEvent) => { const handleProfileSubmit = async (e: React.FormEvent) => {
@@ -223,7 +227,7 @@ export const SettingsPage: React.FC = () => {
try { try {
await CondoService.updateSettings(globalSettings); await CondoService.updateSettings(globalSettings);
setSuccessMsg('Configurazione SMTP salvata!'); setSuccessMsg('Configurazione SMTP salvata!');
setTimeout(() => setSuccessMsg(''), 3000); setTimeout(() => { setSuccessMsg(''); setShowSmtpModal(false); }, 2000);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} finally { } finally {
@@ -492,9 +496,15 @@ export const SettingsPage: React.FC = () => {
const tabs: {id: TabType, label: string, icon: React.ReactNode}[] = [ const tabs: {id: TabType, label: string, icon: React.ReactNode}[] = [
{ id: 'profile', label: 'Profilo', icon: <UserIcon className="w-4 h-4"/> }, { id: 'profile', label: 'Profilo', icon: <UserIcon className="w-4 h-4"/> },
]; ];
if (isAdmin) {
// "Funzionalità" visible ONLY to SuperAdmin
if (isSuperAdmin) {
tabs.push({ id: 'features', label: 'Funzionalità', icon: <LayoutGrid className="w-4 h-4"/> });
}
// Other tabs visible to Privileged (Admin + PowerUser)
if (isPrivileged) {
tabs.push( tabs.push(
{ id: 'features', label: 'Funzionalità', icon: <LayoutGrid className="w-4 h-4"/> },
{ id: 'general', label: 'Condominio', icon: <Building className="w-4 h-4"/> } { id: 'general', label: 'Condominio', icon: <Building className="w-4 h-4"/> }
); );
@@ -512,8 +522,7 @@ export const SettingsPage: React.FC = () => {
} }
tabs.push( tabs.push(
{ id: 'alerts', label: 'Avvisi Email', icon: <Bell className="w-4 h-4"/> }, { id: 'alerts', label: 'Avvisi Email', icon: <Bell className="w-4 h-4"/> }
{ id: 'smtp', label: 'SMTP', icon: <Mail className="w-4 h-4"/> }
); );
} }
@@ -560,8 +569,8 @@ export const SettingsPage: React.FC = () => {
</div> </div>
)} )}
{/* Features Tab */} {/* Features Tab (SUPER ADMIN ONLY) */}
{isAdmin && activeTab === 'features' && globalSettings && ( {isSuperAdmin && activeTab === 'features' && globalSettings && (
<div className="animate-fade-in bg-white rounded-xl shadow-sm border border-slate-200 p-6 max-w-3xl"> <div className="animate-fade-in bg-white rounded-xl shadow-sm border border-slate-200 p-6 max-w-3xl">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-bold text-slate-800 flex items-center gap-2"> <h3 className="text-lg font-bold text-slate-800 flex items-center gap-2">
@@ -627,7 +636,7 @@ export const SettingsPage: React.FC = () => {
)} )}
{/* General Tab */} {/* General Tab */}
{isAdmin && activeTab === 'general' && ( {isPrivileged && activeTab === 'general' && (
<div className="space-y-6 animate-fade-in"> <div className="space-y-6 animate-fade-in">
{!activeCondo ? ( {!activeCondo ? (
<div className="bg-amber-50 border border-amber-200 text-amber-800 p-4 rounded-lg">Nessun condominio selezionato.</div> <div className="bg-amber-50 border border-amber-200 text-amber-800 p-4 rounded-lg">Nessun condominio selezionato.</div>
@@ -688,7 +697,7 @@ export const SettingsPage: React.FC = () => {
)} )}
{/* Condos List Tab */} {/* Condos List Tab */}
{isAdmin && activeTab === 'condos' && ( {isPrivileged && activeTab === 'condos' && (
<div className="space-y-4 animate-fade-in"> <div className="space-y-4 animate-fade-in">
<div className="flex justify-between items-center bg-blue-50 p-4 rounded-xl border border-blue-100"> <div className="flex justify-between items-center bg-blue-50 p-4 rounded-xl border border-blue-100">
<div><h3 className="font-bold text-blue-800">I Tuoi Condomini</h3></div> <div><h3 className="font-bold text-blue-800">I Tuoi Condomini</h3></div>
@@ -714,7 +723,7 @@ export const SettingsPage: React.FC = () => {
)} )}
{/* Families Tab */} {/* Families Tab */}
{isAdmin && activeTab === 'families' && ( {isPrivileged && activeTab === 'families' && (
<div className="space-y-4 animate-fade-in"> <div className="space-y-4 animate-fade-in">
{!activeCondo ? ( {!activeCondo ? (
<div className="p-8 text-center bg-slate-50 rounded-xl border border-dashed border-slate-300"> <div className="p-8 text-center bg-slate-50 rounded-xl border border-dashed border-slate-300">
@@ -764,7 +773,7 @@ export const SettingsPage: React.FC = () => {
)} )}
{/* Users Tab */} {/* Users Tab */}
{isAdmin && activeTab === 'users' && ( {isPrivileged && activeTab === 'users' && (
<div className="space-y-4 animate-fade-in"> <div className="space-y-4 animate-fade-in">
<div className="flex justify-end"> <div className="flex justify-end">
<button <button
@@ -793,7 +802,7 @@ export const SettingsPage: React.FC = () => {
)} )}
{/* NOTICES (BACHECA) TAB */} {/* NOTICES (BACHECA) TAB */}
{isAdmin && activeTab === 'notices' && ( {isPrivileged && activeTab === 'notices' && (
<div className="space-y-4 animate-fade-in"> <div className="space-y-4 animate-fade-in">
<div className="flex justify-between items-center bg-blue-50 p-4 rounded-xl border border-blue-100"> <div className="flex justify-between items-center bg-blue-50 p-4 rounded-xl border border-blue-100">
<div><h3 className="font-bold text-blue-800">Bacheca Condominiale</h3><p className="text-sm text-blue-600">Pubblica avvisi visibili a tutti i condomini.</p></div> <div><h3 className="font-bold text-blue-800">Bacheca Condominiale</h3><p className="text-sm text-blue-600">Pubblica avvisi visibili a tutti i condomini.</p></div>
@@ -848,8 +857,20 @@ export const SettingsPage: React.FC = () => {
)} )}
{/* ALERTS TAB */} {/* ALERTS TAB */}
{isAdmin && activeTab === 'alerts' && ( {isPrivileged && activeTab === 'alerts' && (
<div className="space-y-4 animate-fade-in"> <div className="space-y-6 animate-fade-in">
{/* SMTP Config Button */}
<div className="flex justify-end">
<button
onClick={() => setShowSmtpModal(true)}
className="flex items-center gap-2 bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-2 rounded-lg font-medium border border-slate-200 transition-colors"
>
<Mail className="w-4 h-4" />
Impostazioni SMTP
</button>
</div>
<div className="flex justify-between items-center bg-blue-50 p-4 rounded-xl border border-blue-100"> <div className="flex justify-between items-center bg-blue-50 p-4 rounded-xl border border-blue-100">
<div><h3 className="font-bold text-blue-800">Avvisi Automatici</h3><p className="text-sm text-blue-600">Configura email automatiche per scadenze.</p></div> <div><h3 className="font-bold text-blue-800">Avvisi Automatici</h3><p className="text-sm text-blue-600">Configura email automatiche per scadenze.</p></div>
<button onClick={openAddAlertModal} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2"><Plus className="w-4 h-4" /> Nuovo Avviso</button> <button onClick={openAddAlertModal} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2"><Plus className="w-4 h-4" /> Nuovo Avviso</button>
@@ -875,26 +896,6 @@ export const SettingsPage: React.FC = () => {
</div> </div>
)} )}
{/* SMTP TAB */}
{isAdmin && activeTab === 'smtp' && (
<div className="animate-fade-in bg-white rounded-xl shadow-sm border border-slate-200 p-6 max-w-2xl">
<h3 className="text-lg font-bold text-slate-800 mb-6 flex items-center gap-2"><Server className="w-5 h-5 text-blue-600"/> Configurazione SMTP</h3>
<form onSubmit={handleSmtpSubmit} className="space-y-5">
<div className="grid grid-cols-2 gap-4">
<div><label className="text-sm font-bold text-slate-700">Host</label><input type="text" value={globalSettings?.smtpConfig?.host || ''} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), host: e.target.value} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700"/></div>
<div><label className="text-sm font-bold text-slate-700">Porta</label><input type="number" value={globalSettings?.smtpConfig?.port || 0} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), port: parseInt(e.target.value)} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700"/></div>
</div>
<div className="grid grid-cols-2 gap-4">
<div><label className="text-sm font-bold text-slate-700">Utente</label><input type="text" value={globalSettings?.smtpConfig?.user || ''} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), user: e.target.value} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700"/></div>
<div><label className="text-sm font-bold text-slate-700">Password</label><input type="password" value={globalSettings?.smtpConfig?.pass || ''} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), pass: e.target.value} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700"/></div>
</div>
<div><label className="text-sm font-bold text-slate-700">Email Mittente</label><input type="email" value={globalSettings?.smtpConfig?.fromEmail || ''} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), fromEmail: e.target.value} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700"/></div>
<div className="flex items-center gap-2"><input type="checkbox" checked={globalSettings?.smtpConfig?.secure || false} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), secure: e.target.checked} as any} : null)} className="w-4 h-4"/><label className="text-sm text-slate-700">Usa SSL/TLS (Secure)</label></div>
<div className="pt-2 flex justify-between"><span className="text-green-600">{successMsg}</span><button type="submit" className="bg-blue-600 text-white px-6 py-2.5 rounded-lg hover:bg-blue-700">Salva Configurazione</button></div>
</form>
</div>
)}
{/* ALERT MODAL (Existing) */} {/* ALERT MODAL (Existing) */}
{showAlertModal && ( {showAlertModal && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm"> <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
@@ -932,6 +933,59 @@ export const SettingsPage: React.FC = () => {
</div> </div>
)} )}
{/* SMTP CONFIG MODAL */}
{showSmtpModal && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg p-6 animate-in fade-in zoom-in duration-200">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-bold text-slate-800 flex items-center gap-2">
<Server className="w-5 h-5 text-blue-600"/> Configurazione SMTP
</h3>
<button onClick={() => setShowSmtpModal(false)} className="text-slate-400 hover:text-slate-600"><X className="w-6 h-6"/></button>
</div>
<form onSubmit={handleSmtpSubmit} className="space-y-5">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Host</label>
<input type="text" value={globalSettings?.smtpConfig?.host || ''} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), host: e.target.value} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="smtp.gmail.com"/>
</div>
<div>
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Porta</label>
<input type="number" value={globalSettings?.smtpConfig?.port || 0} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), port: parseInt(e.target.value)} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="587"/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Utente</label>
<input type="text" value={globalSettings?.smtpConfig?.user || ''} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), user: e.target.value} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700"/>
</div>
<div>
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Password</label>
<input type="password" value={globalSettings?.smtpConfig?.pass || ''} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), pass: e.target.value} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700"/>
</div>
</div>
<div>
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Email Mittente</label>
<input type="email" value={globalSettings?.smtpConfig?.fromEmail || ''} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), fromEmail: e.target.value} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="no-reply@condominio.it"/>
</div>
<div className="flex items-center gap-2">
<input type="checkbox" checked={globalSettings?.smtpConfig?.secure || false} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), secure: e.target.checked} as any} : null)} className="w-4 h-4 text-blue-600"/>
<label className="text-sm text-slate-700 font-medium">Usa SSL/TLS (Secure)</label>
</div>
<div className="pt-2 flex justify-between items-center border-t border-slate-100 mt-2">
<span className="text-green-600 text-sm font-medium">{successMsg}</span>
<div className="flex gap-2">
<button type="button" onClick={() => setShowSmtpModal(false)} className="px-4 py-2 text-slate-600 border rounded-lg hover:bg-slate-50">Chiudi</button>
<button type="submit" className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 font-medium">Salva</button>
</div>
</div>
</form>
</div>
</div>
)}
{/* NOTICE MODAL (Existing) */} {/* NOTICE MODAL (Existing) */}
{showNoticeModal && ( {showNoticeModal && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm"> <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">

View File

@@ -89,11 +89,12 @@ const authenticateToken = (req, res, next) => {
}; };
const requireAdmin = (req, res, next) => { const requireAdmin = (req, res, next) => {
if (req.user && req.user.role === 'admin') { // Allow both 'admin' and 'poweruser' to access administrative routes
if (req.user && (req.user.role === 'admin' || req.user.role === 'poweruser')) {
next(); next();
} else { } else {
console.warn(`Access denied for user ${req.user?.email} with role ${req.user?.role}`); console.warn(`Access denied for user ${req.user?.email} with role ${req.user?.role}`);
res.status(403).json({ message: 'Access denied: Admins only' }); res.status(403).json({ message: 'Access denied: Privileged users only' });
} }
}; };