- Switch frontend sign-in to /api/v1/auth/sign-in endpoint (includes persona) - Add updateUser() to App context for proper state management - Fix Settings.tsx save flow to use updateUser() instead of broken localStorage merge - Remove unused web AuthController (gadget-code/src/controllers/auth.ts) - Fix UserApiControllerV1 to return flat user object instead of double-wrapped - Remove SessionType enum and references (dead code) - Add proper server sign-out call before clearing local state Resolves issue where User Settings view didn't display persona text even though it existed in the database.
166 lines
5.3 KiB
TypeScript
166 lines
5.3 KiB
TypeScript
import { useState, useEffect, createContext, useContext } from 'react';
|
|
import { Routes, Route, Navigate, useNavigate } from 'react-router-dom';
|
|
import { User, setOnTokenRefreshed } from './lib/api';
|
|
import { socketClient } from './lib/socket';
|
|
import Header from './components/Header';
|
|
import StatusBar from './components/StatusBar';
|
|
import Home from './pages/Home';
|
|
import ProjectManager from './pages/ProjectManager';
|
|
import SignIn from './pages/SignIn';
|
|
import ChatSessionView from './pages/ChatSessionView';
|
|
import DroneManager from './pages/DroneManager';
|
|
import Settings from './pages/Settings';
|
|
|
|
const TOKEN_KEY = 'dtp_auth_token';
|
|
const USER_KEY = 'dtp_user';
|
|
const PROJECT_KEY = 'dtp_current_project';
|
|
|
|
function getStoredUser(): User | null {
|
|
try {
|
|
const userData = localStorage.getItem(USER_KEY);
|
|
return userData ? JSON.parse(userData) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function setStoredUser(user: User | null) {
|
|
if (user) {
|
|
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
|
} else {
|
|
localStorage.removeItem(USER_KEY);
|
|
}
|
|
}
|
|
|
|
export function getStoredProject(): string | null {
|
|
return localStorage.getItem(PROJECT_KEY);
|
|
}
|
|
|
|
export function setStoredProject(slug: string | null) {
|
|
if (slug) {
|
|
localStorage.setItem(PROJECT_KEY, slug);
|
|
} else {
|
|
localStorage.removeItem(PROJECT_KEY);
|
|
}
|
|
}
|
|
|
|
interface AppContextType {
|
|
user: User | null;
|
|
updateUser: (user: User) => void;
|
|
currentProject: string | null;
|
|
setCurrentProject: (slug: string | null) => void;
|
|
onSignOut: () => void;
|
|
statusMessage: string;
|
|
setStatusMessage: (message: string) => void;
|
|
}
|
|
|
|
export const AppContext = createContext<AppContextType | null>(null);
|
|
|
|
export function useAppContext(): AppContextType {
|
|
const ctx = useContext(AppContext);
|
|
if (!ctx) throw new Error('useAppContext must be used within App provider');
|
|
return ctx;
|
|
}
|
|
|
|
export default function App() {
|
|
const navigate = useNavigate();
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [currentProject, setCurrentProject] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [statusMessage, setStatusMessage] = useState<string>('Ready.');
|
|
|
|
useEffect(() => {
|
|
const storedUser = getStoredUser();
|
|
const storedToken = localStorage.getItem(TOKEN_KEY);
|
|
const storedProject = localStorage.getItem(PROJECT_KEY);
|
|
if (storedUser && storedToken) {
|
|
setUser(storedUser);
|
|
socketClient.connect(storedToken);
|
|
}
|
|
if (storedProject) {
|
|
setCurrentProject(storedProject);
|
|
}
|
|
|
|
// Register callback so the socket client updates its JWT whenever
|
|
// the API client refreshes the token. This ensures that future
|
|
// socket reconnections use the new token instead of the expired one.
|
|
setOnTokenRefreshed((newToken: string) => {
|
|
socketClient.updateJwt(newToken);
|
|
});
|
|
|
|
setLoading(false);
|
|
}, []);
|
|
|
|
const handleSignInSuccess = (newUser: User, token: string) => {
|
|
localStorage.setItem(TOKEN_KEY, token);
|
|
setStoredUser(newUser);
|
|
setUser(newUser);
|
|
socketClient.connect(token);
|
|
};
|
|
|
|
const handleUpdateUser = (updatedUser: User) => {
|
|
setStoredUser(updatedUser);
|
|
setUser(updatedUser);
|
|
};
|
|
|
|
const handleSignOut = async () => {
|
|
try {
|
|
await fetch('/api/v1/auth/sign-out', {
|
|
method: 'GET',
|
|
credentials: 'include',
|
|
});
|
|
} catch {
|
|
// Ignore errors — we're signing out regardless
|
|
}
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
localStorage.removeItem(PROJECT_KEY);
|
|
setStoredUser(null);
|
|
setUser(null);
|
|
setCurrentProject(null);
|
|
socketClient.disconnect();
|
|
navigate('/');
|
|
};
|
|
|
|
const handleSetCurrentProject = (slug: string | null) => {
|
|
setStoredProject(slug);
|
|
setCurrentProject(slug);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="h-screen flex items-center justify-center bg-bg-primary">
|
|
<div className="text-text-muted">Loading...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AppContext.Provider value={{ user, updateUser: handleUpdateUser, currentProject, setCurrentProject: handleSetCurrentProject, onSignOut: handleSignOut, statusMessage, setStatusMessage }}>
|
|
<div className="h-screen flex flex-col bg-bg-primary">
|
|
<Header user={user} onSignOut={handleSignOut} />
|
|
<main className="flex-1 flex overflow-hidden">
|
|
<Routes>
|
|
<Route path="/" element={<Home user={user} />} />
|
|
<Route path="/projects" element={<ProjectManager user={user} />} />
|
|
<Route path="/projects/new" element={<ProjectManager user={user} />} />
|
|
<Route path="/projects/:slug" element={<ProjectManager user={user} />} />
|
|
<Route path="/drones" element={<DroneManager user={user} />} />
|
|
<Route path="/settings" element={user ? <Settings /> : <Navigate to="/sign-in" replace />} />
|
|
<Route path="/projects/:projectId/chat-session/:sessionId" element={<ChatSessionView />} />
|
|
<Route
|
|
path="/sign-in"
|
|
element={
|
|
user ? (
|
|
<Navigate to="/" replace />
|
|
) : (
|
|
<SignIn onSuccess={handleSignInSuccess} />
|
|
)
|
|
}
|
|
/>
|
|
</Routes>
|
|
</main>
|
|
<StatusBar statusMessage={statusMessage} projectSlug={currentProject} />
|
|
</div>
|
|
</AppContext.Provider>
|
|
);
|
|
} |