feat(dashboard): header user dropdown (Syncfusion) + proper logout; docs: clarify architecture; build: add splitbuttons; bump alpha.10
Dashboard Add top-right user dropdown using Syncfusion DropDownButton: shows username + role; menu entries “Profil” and “Abmelden”. Replace custom dropdown logic with Syncfusion component; position at header’s right edge. Update /logout page to call backend logout and redirect to /login (reliable user switching). Build/Config Add @syncfusion/ej2-react-splitbuttons and @syncfusion/ej2-splitbuttons dependencies. Update Vite optimizeDeps.include to pre-bundle splitbuttons and avoid import-analysis errors. Docs README: Rework Architecture Overview with clearer data flow: Listener consumes MQTT (discovery/heartbeats) and updates API. Scheduler reads from API and publishes events via MQTT to clients. Clients send via MQTT and receive via MQTT. Worker receives commands directly from API and reports results back (no MQTT). Explicit note: MariaDB is accessed exclusively by the API Server; Dashboard never talks to DB directly. README: Add SplitButtons to “Syncfusion Components Used”; add troubleshooting steps for @syncfusion/ej2-react-splitbuttons import issues (optimizeDeps + volume reset). Copilot instructions: Document header user menu and splitbuttons technical notes (deps, optimizeDeps, dev-container node_modules volume). Program info Bump to 2025.1.0-alpha.10 with changelog: UI: Header user menu (DropDownButton with username/role; Profil/Abmelden). Frontend: Syncfusion SplitButtons integration + Vite pre-bundling config. Fix: Added README guidance for splitbuttons import errors. No breaking changes.
This commit is contained in:
145
dashboard/src/useAuth.tsx
Normal file
145
dashboard/src/useAuth.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Auth context and hook for managing current user state.
|
||||
*
|
||||
* Provides a React context and custom hook to access and manage
|
||||
* the current authenticated user throughout the application.
|
||||
*/
|
||||
|
||||
import { createContext, useContext, useState, useEffect } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { fetchCurrentUser, login as apiLogin, logout as apiLogout } from './apiAuth';
|
||||
import type { User } from './apiAuth';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
refreshUser: () => Promise<void>;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth provider component to wrap the application.
|
||||
*
|
||||
* Usage:
|
||||
* <AuthProvider>
|
||||
* <App />
|
||||
* </AuthProvider>
|
||||
*/
|
||||
export function AuthProvider({ children }: AuthProviderProps) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Fetch current user on mount
|
||||
useEffect(() => {
|
||||
refreshUser();
|
||||
}, []);
|
||||
|
||||
const refreshUser = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const currentUser = await fetchCurrentUser();
|
||||
setUser(currentUser);
|
||||
} catch (err) {
|
||||
// Not authenticated or error - this is okay
|
||||
setUser(null);
|
||||
// Only set error if it's not a 401 (not authenticated is expected)
|
||||
if (err instanceof Error && !err.message.includes('Not authenticated')) {
|
||||
setError(err.message);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await apiLogin(username, password);
|
||||
setUser(response.user as User);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Login failed';
|
||||
setError(errorMessage);
|
||||
throw err; // Re-throw so the caller can handle it
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
await apiLogout();
|
||||
setUser(null);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Logout failed';
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
loading,
|
||||
error,
|
||||
login,
|
||||
logout,
|
||||
refreshUser,
|
||||
isAuthenticated: user !== null,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to access auth context.
|
||||
*
|
||||
* Usage:
|
||||
* const { user, login, logout, isAuthenticated } = useAuth();
|
||||
*
|
||||
* @returns AuthContextType
|
||||
* @throws Error if used outside AuthProvider
|
||||
*/
|
||||
export function useAuth(): AuthContextType {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience hook to get just the current user.
|
||||
*
|
||||
* Usage:
|
||||
* const user = useCurrentUser();
|
||||
*/
|
||||
export function useCurrentUser(): User | null {
|
||||
const { user } = useAuth();
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience hook to check if user is authenticated.
|
||||
*
|
||||
* Usage:
|
||||
* const isAuthenticated = useIsAuthenticated();
|
||||
*/
|
||||
export function useIsAuthenticated(): boolean {
|
||||
const { isAuthenticated } = useAuth();
|
||||
return isAuthenticated;
|
||||
}
|
||||
Reference in New Issue
Block a user