gadget/gadget-code/frontend/src/App.tsx
Rob Colbert 896aff1b02 Fix User Settings persona display issue
- 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.
2026-05-12 01:15:00 -04:00

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>
);
}