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:
@@ -6,12 +6,13 @@ import { Save, Building, Coins, Plus, Pencil, Trash2, X, CalendarCheck, AlertTri
|
||||
|
||||
export const SettingsPage: React.FC = () => {
|
||||
const currentUser = CondoService.getCurrentUser();
|
||||
const isAdmin = currentUser?.role === 'admin';
|
||||
const isSuperAdmin = currentUser?.role === 'admin';
|
||||
const isPrivileged = currentUser?.role === 'admin' || currentUser?.role === 'poweruser';
|
||||
|
||||
// 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);
|
||||
|
||||
// Profile State
|
||||
@@ -87,6 +88,9 @@ export const SettingsPage: React.FC = () => {
|
||||
sendHour: 9,
|
||||
active: true
|
||||
});
|
||||
|
||||
// SMTP Modal State
|
||||
const [showSmtpModal, setShowSmtpModal] = useState(false);
|
||||
|
||||
// Notices (Bacheca) State
|
||||
const [notices, setNotices] = useState<Notice[]>([]);
|
||||
@@ -116,7 +120,7 @@ export const SettingsPage: React.FC = () => {
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
if (isAdmin) {
|
||||
if (isPrivileged) {
|
||||
// First fetch global/structural data
|
||||
const condoList = await CondoService.getCondos();
|
||||
const activeC = await CondoService.getActiveCondo();
|
||||
@@ -165,7 +169,7 @@ export const SettingsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [isAdmin]);
|
||||
}, [isPrivileged]);
|
||||
|
||||
// --- Profile Handlers ---
|
||||
const handleProfileSubmit = async (e: React.FormEvent) => {
|
||||
@@ -223,7 +227,7 @@ export const SettingsPage: React.FC = () => {
|
||||
try {
|
||||
await CondoService.updateSettings(globalSettings);
|
||||
setSuccessMsg('Configurazione SMTP salvata!');
|
||||
setTimeout(() => setSuccessMsg(''), 3000);
|
||||
setTimeout(() => { setSuccessMsg(''); setShowSmtpModal(false); }, 2000);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
@@ -492,9 +496,15 @@ export const SettingsPage: React.FC = () => {
|
||||
const tabs: {id: TabType, label: string, icon: React.ReactNode}[] = [
|
||||
{ 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(
|
||||
{ id: 'features', label: 'Funzionalità', icon: <LayoutGrid 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(
|
||||
{ id: 'alerts', label: 'Avvisi Email', icon: <Bell className="w-4 h-4"/> },
|
||||
{ id: 'smtp', label: 'SMTP', icon: <Mail className="w-4 h-4"/> }
|
||||
{ id: 'alerts', label: 'Avvisi Email', icon: <Bell className="w-4 h-4"/> }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -560,8 +569,8 @@ export const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Features Tab */}
|
||||
{isAdmin && activeTab === 'features' && globalSettings && (
|
||||
{/* Features Tab (SUPER ADMIN ONLY) */}
|
||||
{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="flex items-center justify-between mb-6">
|
||||
<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 */}
|
||||
{isAdmin && activeTab === 'general' && (
|
||||
{isPrivileged && activeTab === 'general' && (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{!activeCondo ? (
|
||||
<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 */}
|
||||
{isAdmin && activeTab === 'condos' && (
|
||||
{isPrivileged && activeTab === 'condos' && (
|
||||
<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><h3 className="font-bold text-blue-800">I Tuoi Condomini</h3></div>
|
||||
@@ -714,7 +723,7 @@ export const SettingsPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Families Tab */}
|
||||
{isAdmin && activeTab === 'families' && (
|
||||
{isPrivileged && activeTab === 'families' && (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
{!activeCondo ? (
|
||||
<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 */}
|
||||
{isAdmin && activeTab === 'users' && (
|
||||
{isPrivileged && activeTab === 'users' && (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
@@ -793,7 +802,7 @@ export const SettingsPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* NOTICES (BACHECA) TAB */}
|
||||
{isAdmin && activeTab === 'notices' && (
|
||||
{isPrivileged && activeTab === 'notices' && (
|
||||
<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><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 */}
|
||||
{isAdmin && activeTab === 'alerts' && (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
{isPrivileged && activeTab === 'alerts' && (
|
||||
<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><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>
|
||||
@@ -875,26 +896,6 @@ export const SettingsPage: React.FC = () => {
|
||||
</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) */}
|
||||
{showAlertModal && (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
||||
@@ -931,6 +932,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) */}
|
||||
{showNoticeModal && (
|
||||
@@ -1194,4 +1248,4 @@ export const SettingsPage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -89,11 +89,12 @@ const authenticateToken = (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();
|
||||
} else {
|
||||
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' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user