[SPRINT-1] feat: Resolve routing and improve auth session management
SUBTASK-001: Routing fixes - Add lazy-loaded route for /portfolio/:portfolioId - Add navigation links from PortfolioDashboard to portfolio detail - Verify /settings/billing is intentional dual-route (no changes needed) SUBTASK-002: Auth improvements - Extend ActiveSession type with device details (deviceType, browser, os, location) - DeviceCard now uses backend data when available, falls back to userAgent parsing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e639f36a22
commit
295bd5e31e
@ -41,6 +41,7 @@ const Assistant = lazy(() => import('./modules/assistant/pages/Assistant'));
|
||||
|
||||
// Lazy load modules - Portfolio
|
||||
const PortfolioDashboard = lazy(() => import('./modules/portfolio/pages/PortfolioDashboard'));
|
||||
const PortfolioDetailPage = lazy(() => import('./modules/portfolio/pages/PortfolioDetailPage'));
|
||||
const CreatePortfolio = lazy(() => import('./modules/portfolio/pages/CreatePortfolio'));
|
||||
const CreateGoal = lazy(() => import('./modules/portfolio/pages/CreateGoal'));
|
||||
const EditAllocations = lazy(() => import('./modules/portfolio/pages/EditAllocations'));
|
||||
@ -112,6 +113,7 @@ function App() {
|
||||
<Route path="/portfolio" element={<PortfolioDashboard />} />
|
||||
<Route path="/portfolio/new" element={<CreatePortfolio />} />
|
||||
<Route path="/portfolio/goals/new" element={<CreateGoal />} />
|
||||
<Route path="/portfolio/:portfolioId" element={<PortfolioDetailPage />} />
|
||||
<Route path="/portfolio/:portfolioId/edit" element={<EditAllocations />} />
|
||||
|
||||
{/* Education */}
|
||||
|
||||
@ -55,9 +55,21 @@ interface DeviceCardProps {
|
||||
|
||||
export function DeviceCard({ session, isRevoking, onRevoke }: DeviceCardProps) {
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const deviceInfo = authService.parseUserAgent(session.userAgent);
|
||||
const parsedInfo = authService.parseUserAgent(session.userAgent);
|
||||
const relativeTime = authService.formatRelativeTime(session.lastActiveAt);
|
||||
|
||||
// Use backend-provided device info if available, otherwise use parsed info
|
||||
const deviceInfo = {
|
||||
type: (session.deviceType as 'desktop' | 'mobile' | 'tablet' | 'unknown') || parsedInfo.type,
|
||||
os: session.os || parsedInfo.os,
|
||||
browser: session.browser || parsedInfo.browser,
|
||||
};
|
||||
|
||||
// Format location from backend data
|
||||
const location = session.city && session.countryCode
|
||||
? `${session.city}, ${session.countryCode}`
|
||||
: session.countryCode || null;
|
||||
|
||||
// Get device icon based on type
|
||||
const DeviceIcon = {
|
||||
desktop: DesktopIcon,
|
||||
@ -117,7 +129,7 @@ export function DeviceCard({ session, isRevoking, onRevoke }: DeviceCardProps) {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
{session.ipAddress || 'Unknown IP'}
|
||||
{location ? `${location} (${session.ipAddress || 'Unknown IP'})` : session.ipAddress || 'Unknown IP'}
|
||||
</span>
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
|
||||
@ -195,17 +195,29 @@ export default function PortfolioDashboard() {
|
||||
{portfolios.length > 1 && (
|
||||
<div className="flex gap-2 mb-6 overflow-x-auto pb-2">
|
||||
{portfolios.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => selectPortfolio(p)}
|
||||
className={`px-4 py-2 rounded-lg whitespace-nowrap transition-colors ${
|
||||
selectedPortfolio?.id === p.id
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
{p.name}
|
||||
</button>
|
||||
<div key={p.id} className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => selectPortfolio(p)}
|
||||
className={`px-4 py-2 rounded-l-lg whitespace-nowrap transition-colors ${
|
||||
selectedPortfolio?.id === p.id
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
{p.name}
|
||||
</button>
|
||||
<Link
|
||||
to={`/portfolio/${p.id}`}
|
||||
className={`px-2 py-2 rounded-r-lg whitespace-nowrap transition-colors ${
|
||||
selectedPortfolio?.id === p.id
|
||||
? 'bg-blue-700 text-white hover:bg-blue-800'
|
||||
: 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-500'
|
||||
}`}
|
||||
title="Ver detalle"
|
||||
>
|
||||
<ArrowTrendingUpIcon className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@ -388,6 +400,12 @@ export default function PortfolioDashboard() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
to={`/portfolio/${selectedPortfolio.id}`}
|
||||
className="mt-4 w-full block text-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
|
||||
>
|
||||
Ver Detalle Completo
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -14,6 +14,11 @@ export interface ActiveSession {
|
||||
id: string;
|
||||
userAgent: string;
|
||||
ipAddress: string;
|
||||
deviceType?: string;
|
||||
browser?: string;
|
||||
os?: string;
|
||||
countryCode?: string;
|
||||
city?: string;
|
||||
createdAt: string;
|
||||
lastActiveAt: string;
|
||||
isCurrent: boolean;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user