Add comprehensive backend features and mobile UI improvements

Backend:
- Add 2FA authentication with TOTP support
- Add API keys management system
- Add audit logging for security events
- Add file upload/management system
- Add notifications system with preferences
- Add session management
- Add webhooks integration
- Add analytics endpoints
- Add export functionality
- Add password policy enforcement
- Add new database migrations for core tables

Frontend:
- Add module position system (top/bottom sidebar sections)
- Add search and notifications module configuration tabs
- Add mobile logo replacing hamburger menu
- Center page title absolutely when no tabs present
- Align sidebar footer toggles with navigation items
- Add lighter icon color in dark theme for mobile
- Add API keys management page
- Add notifications page with context
- Add admin analytics and audit logs pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-17 22:27:32 +01:00
parent f698aa4d51
commit 8c4a555b88
76 changed files with 9751 additions and 323 deletions

View File

@@ -7,13 +7,13 @@ import type { ModuleId } from '../../contexts/ModulesContext';
import Feature1Tab from '../../components/admin/Feature1Tab';
import '../../styles/AdminPanel.css';
type TabId = 'config' | 'feature1' | 'feature2' | 'feature3';
type TabId = 'config' | 'feature1' | 'feature2' | 'feature3' | 'search' | 'notifications';
export default function Features() {
const { user: currentUser } = useAuth();
const { t } = useTranslation();
const { toggleMobileMenu } = useSidebar();
const { moduleStates, moduleOrder, setModuleEnabled, setModuleOrder, saveModulesToBackend, saveModuleOrder, hasInitialized, isLoading } = useModules();
const { moduleStates, moduleOrder, setModuleEnabled, setModulePosition, setModuleOrder, saveModulesToBackend, saveModuleOrder, hasInitialized, isLoading } = useModules();
const [activeTab, setActiveTab] = useState<TabId>('config');
const hasUserMadeChanges = useRef(false);
const saveRef = useRef(saveModulesToBackend);
@@ -63,47 +63,44 @@ export default function Features() {
};
}, []);
const getModuleDescription = (moduleId: string): string => {
const key = `${moduleId}Desc` as keyof typeof t.admin;
return t.admin[key] || t.admin.moduleDefaultDesc;
};
const renderModuleToggle = (moduleId: ModuleId) => {
const state = moduleStates[moduleId];
const adminEnabled = state?.admin ?? true;
const userEnabled = state?.user ?? true;
return (
<div className="feature-header">
<div className="feature-header-info">
<p>{getModuleDescription(moduleId)}</p>
</div>
<div className="feature-header-actions">
<div className={`feature-status-badge ${adminEnabled ? 'active' : ''}`}>
{adminEnabled ? t.admin.active : t.admin.inactive}
<div className="theme-tab-content">
<div className="theme-section">
<div className="section-header">
<h3 className="section-title">{t.featuresPage?.visibility || 'Visibilità'}</h3>
</div>
<div className="toggle-group">
<span className="toggle-label">{t.admin.adminRole}</span>
<label className="toggle-modern">
<input
type="checkbox"
checked={adminEnabled}
onChange={(e) => handleModuleToggle(moduleId, 'admin', e.target.checked)}
/>
<span className="toggle-slider-modern"></span>
</label>
</div>
<div className="toggle-group">
<span className="toggle-label">{t.admin.userRole}</span>
<label className={`toggle-modern ${!adminEnabled ? 'disabled' : ''}`}>
<input
type="checkbox"
checked={userEnabled}
disabled={!adminEnabled}
onChange={(e) => handleModuleToggle(moduleId, 'user', e.target.checked)}
/>
<span className="toggle-slider-modern"></span>
</label>
<div className="feature-config-options">
<div className={`feature-status-badge ${adminEnabled ? 'active' : ''}`}>
{adminEnabled ? t.admin.active : t.admin.inactive}
</div>
<div className="toggle-group">
<span className="toggle-label">{t.admin.adminRole}</span>
<label className="toggle-modern">
<input
type="checkbox"
checked={adminEnabled}
onChange={(e) => handleModuleToggle(moduleId, 'admin', e.target.checked)}
/>
<span className="toggle-slider-modern"></span>
</label>
</div>
<div className="toggle-group">
<span className="toggle-label">{t.admin.userRole}</span>
<label className={`toggle-modern ${!adminEnabled ? 'disabled' : ''}`}>
<input
type="checkbox"
checked={userEnabled}
disabled={!adminEnabled}
onChange={(e) => handleModuleToggle(moduleId, 'user', e.target.checked)}
/>
<span className="toggle-slider-modern"></span>
</label>
</div>
</div>
</div>
</div>
@@ -131,17 +128,35 @@ export default function Features() {
e.dataTransfer.dropEffect = 'move';
};
const handleDrop = (e: React.DragEvent, targetModuleId: string) => {
const handleDrop = (e: React.DragEvent, targetModuleId: string, targetSection: 'top' | 'bottom') => {
e.preventDefault();
if (!draggedItem || draggedItem === targetModuleId) return;
e.stopPropagation();
if (!draggedItem) return;
const draggedPosition = moduleStates[draggedItem as ModuleId]?.position || 'top';
// If dropping on same item, just change section if different
if (draggedItem === targetModuleId) {
if (draggedPosition !== targetSection) {
hasUserMadeChanges.current = true;
setModulePosition(draggedItem as ModuleId, targetSection);
}
return;
}
// Change position if moving to different section
if (draggedPosition !== targetSection) {
hasUserMadeChanges.current = true;
setModulePosition(draggedItem as ModuleId, targetSection);
}
// Reorder within the list
const newOrder = [...localOrder];
const draggedIndex = newOrder.indexOf(draggedItem);
const targetIndex = newOrder.indexOf(targetModuleId);
if (draggedIndex === -1 || targetIndex === -1) return;
// Remove dragged item and insert at target position
newOrder.splice(draggedIndex, 1);
newOrder.splice(targetIndex, 0, draggedItem);
@@ -149,6 +164,19 @@ export default function Features() {
setHasOrderChanges(true);
};
const handleSectionDrop = (e: React.DragEvent, section: 'top' | 'bottom') => {
e.preventDefault();
if (!draggedItem) return;
const draggedPosition = moduleStates[draggedItem as ModuleId]?.position || 'top';
// Change position if moving to different section
if (draggedPosition !== section) {
hasUserMadeChanges.current = true;
setModulePosition(draggedItem as ModuleId, section);
}
};
const handleApplyOrder = async () => {
try {
setModuleOrder(localOrder);
@@ -164,15 +192,30 @@ export default function Features() {
return module || { id: moduleId, icon: 'extension', defaultEnabled: true };
};
// Split modules by position for the config tab
const topOrderModules = localOrder.filter(id => {
const state = moduleStates[id as ModuleId];
return !state || state.position === 'top';
});
const bottomOrderModules = localOrder.filter(id => {
const state = moduleStates[id as ModuleId];
return state && state.position === 'bottom';
});
const renderConfigTab = () => {
return (
<div className="theme-tab-content">
<div className="theme-section">
<div className="section-header">
<h3 className="section-title">{t.featuresPage?.orderSection || 'Ordine nella Sidebar'}</h3>
<h3 className="section-title">{t.featuresPage?.topSection || 'Sezione Principale'}</h3>
</div>
<div className="order-cards">
{localOrder.map((moduleId, index) => {
<div
className="order-cards"
onDragOver={handleDragOver}
onDrop={(e) => handleSectionDrop(e, 'top')}
>
{topOrderModules.map((moduleId) => {
const moduleInfo = getModuleInfo(moduleId);
const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId;
return (
@@ -183,34 +226,76 @@ export default function Features() {
onDragStart={(e) => handleDragStart(e, moduleId)}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, moduleId)}
onDrop={(e) => handleDrop(e, moduleId, 'top')}
>
<div className="order-card-preview">
<span className="order-card-number">{index + 1}</span>
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
</div>
<div className="order-card-info">
<span className="order-card-name">{moduleName}</span>
<span className="order-card-desc">{t.featuresPage?.orderDesc || 'Trascina per riordinare'}</span>
</div>
<div className="order-card-icon">
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
</div>
<div className="order-card-handle">
<span className="material-symbols-outlined">drag_indicator</span>
</div>
</div>
);
})}
{topOrderModules.length === 0 && (
<div className="order-empty">{t.featuresPage?.noModulesTop || 'Nessun modulo in questa sezione'}</div>
)}
</div>
{hasOrderChanges && (
<div className="order-actions">
<button className="btn-primary" onClick={handleApplyOrder}>
<span className="material-symbols-outlined">check</span>
{t.featuresPage?.applyOrder || 'Applica'}
</button>
</div>
)}
</div>
<div className="theme-section">
<div className="section-header">
<h3 className="section-title">{t.featuresPage?.bottomSection || 'Sezione Inferiore'}</h3>
</div>
<div
className="order-cards"
onDragOver={handleDragOver}
onDrop={(e) => handleSectionDrop(e, 'bottom')}
>
{bottomOrderModules.map((moduleId) => {
const moduleInfo = getModuleInfo(moduleId);
const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId;
return (
<div
key={moduleId}
className={`order-card ${draggedItem === moduleId ? 'dragging' : ''}`}
draggable
onDragStart={(e) => handleDragStart(e, moduleId)}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, moduleId, 'bottom')}
>
<div className="order-card-preview">
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
</div>
<div className="order-card-info">
<span className="order-card-name">{moduleName}</span>
<span className="order-card-desc">{t.featuresPage?.orderDesc || 'Trascina per riordinare'}</span>
</div>
<div className="order-card-handle">
<span className="material-symbols-outlined">drag_indicator</span>
</div>
</div>
);
})}
{bottomOrderModules.length === 0 && (
<div className="order-empty">{t.featuresPage?.noModulesBottom || 'Nessun modulo in questa sezione'}</div>
)}
</div>
</div>
{hasOrderChanges && (
<div className="order-actions">
<button className="btn-primary" onClick={handleApplyOrder}>
<span className="material-symbols-outlined">check</span>
{t.featuresPage?.applyOrder || 'Applica'}
</button>
</div>
)}
</div>
);
};
@@ -255,6 +340,32 @@ export default function Features() {
</div>
</>
);
case 'search':
return (
<>
{renderModuleToggle('search')}
<div className="tab-content-placeholder">
<div className="placeholder-icon">
<span className="material-symbols-outlined">search</span>
</div>
<h3>{t.sidebar.search}</h3>
<p>{t.features.comingSoon}</p>
</div>
</>
);
case 'notifications':
return (
<>
{renderModuleToggle('notifications')}
<div className="tab-content-placeholder">
<div className="placeholder-icon">
<span className="material-symbols-outlined">notifications</span>
</div>
<h3>{t.sidebar.notifications}</h3>
<p>{t.features.comingSoon}</p>
</div>
</>
);
default:
return null;
}
@@ -279,16 +390,15 @@ export default function Features() {
<span className="material-symbols-outlined">tune</span>
<span>{t.featuresPage?.configTab || 'Configurazione'}</span>
</button>
{(localOrder.length > 0 ? localOrder : ['feature1', 'feature2', 'feature3']).map((moduleId) => {
const moduleInfo = getModuleInfo(moduleId);
const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId;
{TOGGLEABLE_MODULES.map((module) => {
const moduleName = t.sidebar[module.id as keyof typeof t.sidebar] || module.id;
return (
<button
key={moduleId}
className={`page-tab-btn ${activeTab === moduleId ? 'active' : ''}`}
onClick={() => setActiveTab(moduleId as TabId)}
key={module.id}
className={`page-tab-btn ${activeTab === module.id ? 'active' : ''}`}
onClick={() => setActiveTab(module.id as TabId)}
>
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
<span className="material-symbols-outlined">{module.icon}</span>
<span>{moduleName}</span>
</button>
);