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:
@@ -6,20 +6,24 @@ import type { UserPermissions } from '../types';
|
||||
|
||||
// User-facing modules that can be toggled
|
||||
export const TOGGLEABLE_MODULES = [
|
||||
{ id: 'feature1', icon: 'playlist_play', defaultEnabled: true },
|
||||
{ id: 'feature2', icon: 'download', defaultEnabled: true },
|
||||
{ id: 'feature3', icon: 'cast', defaultEnabled: true },
|
||||
{ id: 'feature1', icon: 'playlist_play', defaultEnabled: true, defaultPosition: 'top' as const },
|
||||
{ id: 'feature2', icon: 'download', defaultEnabled: true, defaultPosition: 'top' as const },
|
||||
{ id: 'feature3', icon: 'cast', defaultEnabled: true, defaultPosition: 'top' as const },
|
||||
{ id: 'search', icon: 'search', defaultEnabled: true, defaultPosition: 'bottom' as const },
|
||||
{ id: 'notifications', icon: 'notifications', defaultEnabled: true, defaultPosition: 'bottom' as const },
|
||||
] as const;
|
||||
|
||||
export type ModuleId = typeof TOGGLEABLE_MODULES[number]['id'];
|
||||
export type ModulePosition = 'top' | 'bottom';
|
||||
|
||||
export interface ModuleState {
|
||||
admin: boolean;
|
||||
user: boolean;
|
||||
position: ModulePosition;
|
||||
}
|
||||
|
||||
// Default order for modules
|
||||
const DEFAULT_MODULE_ORDER: string[] = ['feature1', 'feature2', 'feature3'];
|
||||
// Default order for modules (top modules, then bottom modules)
|
||||
const DEFAULT_MODULE_ORDER: string[] = ['feature1', 'feature2', 'feature3', 'search', 'notifications'];
|
||||
|
||||
interface ModulesContextType {
|
||||
moduleStates: Record<ModuleId, ModuleState>;
|
||||
@@ -27,6 +31,7 @@ interface ModulesContextType {
|
||||
isModuleEnabled: (moduleId: string) => boolean;
|
||||
isModuleEnabledForUser: (moduleId: string, userPermissions: UserPermissions | undefined, isSuperuser: boolean) => boolean;
|
||||
setModuleEnabled: (moduleId: ModuleId, type: 'admin' | 'user', enabled: boolean) => void;
|
||||
setModulePosition: (moduleId: ModuleId, position: ModulePosition) => void;
|
||||
setModuleOrder: (order: string[]) => void;
|
||||
saveModulesToBackend: () => Promise<void>;
|
||||
saveModuleOrder: (order: string[]) => Promise<void>;
|
||||
@@ -40,7 +45,7 @@ const ModulesContext = createContext<ModulesContextType | undefined>(undefined);
|
||||
const getDefaultStates = (): Record<ModuleId, ModuleState> => {
|
||||
const states: Record<string, ModuleState> = {};
|
||||
TOGGLEABLE_MODULES.forEach(m => {
|
||||
states[m.id] = { admin: m.defaultEnabled, user: m.defaultEnabled };
|
||||
states[m.id] = { admin: m.defaultEnabled, user: m.defaultEnabled, position: m.defaultPosition };
|
||||
});
|
||||
return states as Record<ModuleId, ModuleState>;
|
||||
};
|
||||
@@ -58,17 +63,18 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
const settings = await settingsAPI.getModules();
|
||||
const newStates = { ...getDefaultStates() };
|
||||
|
||||
// Helper to safely parse boolean
|
||||
const parseBool = (val: any, defaultVal: boolean): boolean => {
|
||||
if (val === undefined || val === null) return defaultVal;
|
||||
if (val === true || val === 'true' || val === 1 || val === '1') return true;
|
||||
if (val === false || val === 'false' || val === 0 || val === '0') return false;
|
||||
return defaultVal;
|
||||
};
|
||||
|
||||
TOGGLEABLE_MODULES.forEach(m => {
|
||||
const adminKey = `module_${m.id}_admin_enabled`;
|
||||
const userKey = `module_${m.id}_user_enabled`;
|
||||
|
||||
// Helper to safely parse boolean
|
||||
const parseBool = (val: any, defaultVal: boolean): boolean => {
|
||||
if (val === undefined || val === null) return defaultVal;
|
||||
if (val === true || val === 'true' || val === 1 || val === '1') return true;
|
||||
if (val === false || val === 'false' || val === 0 || val === '0') return false;
|
||||
return defaultVal;
|
||||
};
|
||||
const positionKey = `module_${m.id}_position`;
|
||||
|
||||
// Check for new keys
|
||||
// If key exists in settings, use it (parsed). If not, use defaultEnabled.
|
||||
@@ -85,6 +91,13 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
newStates[m.id].user = m.defaultEnabled;
|
||||
}
|
||||
|
||||
// Load position
|
||||
if (settings[positionKey] !== undefined) {
|
||||
newStates[m.id].position = settings[positionKey] === 'bottom' ? 'bottom' : 'top';
|
||||
} else {
|
||||
newStates[m.id].position = m.defaultPosition;
|
||||
}
|
||||
|
||||
// Fallback for backward compatibility (if old key exists)
|
||||
const oldKey = `module_${m.id}_enabled`;
|
||||
if (settings[oldKey] !== undefined && settings[adminKey] === undefined) {
|
||||
@@ -110,6 +123,12 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
} else {
|
||||
order = DEFAULT_MODULE_ORDER;
|
||||
}
|
||||
// Ensure all toggleable modules are included (for newly added modules)
|
||||
const allModuleIds = TOGGLEABLE_MODULES.map(m => m.id);
|
||||
const missingModules = allModuleIds.filter(id => !order.includes(id));
|
||||
if (missingModules.length > 0) {
|
||||
order = [...order, ...missingModules];
|
||||
}
|
||||
setModuleOrderState(order);
|
||||
}
|
||||
|
||||
@@ -125,10 +144,11 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
// Save module settings to backend
|
||||
const saveModulesToBackend = useCallback(async () => {
|
||||
try {
|
||||
const data: Record<string, boolean> = {};
|
||||
const data: Record<string, boolean | string> = {};
|
||||
TOGGLEABLE_MODULES.forEach(m => {
|
||||
data[`module_${m.id}_admin_enabled`] = moduleStates[m.id].admin;
|
||||
data[`module_${m.id}_user_enabled`] = moduleStates[m.id].user;
|
||||
data[`module_${m.id}_position`] = moduleStates[m.id].position;
|
||||
});
|
||||
await settingsAPI.updateModules(data);
|
||||
} catch (error) {
|
||||
@@ -221,6 +241,14 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setModulePosition = useCallback((moduleId: ModuleId, position: ModulePosition) => {
|
||||
setModuleStates(prev => {
|
||||
const newState = { ...prev };
|
||||
newState[moduleId] = { ...newState[moduleId], position };
|
||||
return newState;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ModulesContext.Provider
|
||||
value={{
|
||||
@@ -229,6 +257,7 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
|
||||
isModuleEnabled,
|
||||
isModuleEnabledForUser,
|
||||
setModuleEnabled,
|
||||
setModulePosition,
|
||||
setModuleOrder,
|
||||
saveModulesToBackend,
|
||||
saveModuleOrder,
|
||||
|
||||
Reference in New Issue
Block a user