[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
|
// Lazy load modules - Portfolio
|
||||||
const PortfolioDashboard = lazy(() => import('./modules/portfolio/pages/PortfolioDashboard'));
|
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 CreatePortfolio = lazy(() => import('./modules/portfolio/pages/CreatePortfolio'));
|
||||||
const CreateGoal = lazy(() => import('./modules/portfolio/pages/CreateGoal'));
|
const CreateGoal = lazy(() => import('./modules/portfolio/pages/CreateGoal'));
|
||||||
const EditAllocations = lazy(() => import('./modules/portfolio/pages/EditAllocations'));
|
const EditAllocations = lazy(() => import('./modules/portfolio/pages/EditAllocations'));
|
||||||
@ -112,6 +113,7 @@ function App() {
|
|||||||
<Route path="/portfolio" element={<PortfolioDashboard />} />
|
<Route path="/portfolio" element={<PortfolioDashboard />} />
|
||||||
<Route path="/portfolio/new" element={<CreatePortfolio />} />
|
<Route path="/portfolio/new" element={<CreatePortfolio />} />
|
||||||
<Route path="/portfolio/goals/new" element={<CreateGoal />} />
|
<Route path="/portfolio/goals/new" element={<CreateGoal />} />
|
||||||
|
<Route path="/portfolio/:portfolioId" element={<PortfolioDetailPage />} />
|
||||||
<Route path="/portfolio/:portfolioId/edit" element={<EditAllocations />} />
|
<Route path="/portfolio/:portfolioId/edit" element={<EditAllocations />} />
|
||||||
|
|
||||||
{/* Education */}
|
{/* Education */}
|
||||||
|
|||||||
@ -55,9 +55,21 @@ interface DeviceCardProps {
|
|||||||
|
|
||||||
export function DeviceCard({ session, isRevoking, onRevoke }: DeviceCardProps) {
|
export function DeviceCard({ session, isRevoking, onRevoke }: DeviceCardProps) {
|
||||||
const [showConfirm, setShowConfirm] = useState(false);
|
const [showConfirm, setShowConfirm] = useState(false);
|
||||||
const deviceInfo = authService.parseUserAgent(session.userAgent);
|
const parsedInfo = authService.parseUserAgent(session.userAgent);
|
||||||
const relativeTime = authService.formatRelativeTime(session.lastActiveAt);
|
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
|
// Get device icon based on type
|
||||||
const DeviceIcon = {
|
const DeviceIcon = {
|
||||||
desktop: DesktopIcon,
|
desktop: DesktopIcon,
|
||||||
@ -117,7 +129,7 @@ export function DeviceCard({ session, isRevoking, onRevoke }: DeviceCardProps) {
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||||
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
{session.ipAddress || 'Unknown IP'}
|
{location ? `${location} (${session.ipAddress || 'Unknown IP'})` : session.ipAddress || 'Unknown IP'}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="flex items-center gap-2">
|
<p className="flex items-center gap-2">
|
||||||
|
|||||||
@ -195,17 +195,29 @@ export default function PortfolioDashboard() {
|
|||||||
{portfolios.length > 1 && (
|
{portfolios.length > 1 && (
|
||||||
<div className="flex gap-2 mb-6 overflow-x-auto pb-2">
|
<div className="flex gap-2 mb-6 overflow-x-auto pb-2">
|
||||||
{portfolios.map((p) => (
|
{portfolios.map((p) => (
|
||||||
<button
|
<div key={p.id} className="flex items-center gap-1">
|
||||||
key={p.id}
|
<button
|
||||||
onClick={() => selectPortfolio(p)}
|
onClick={() => selectPortfolio(p)}
|
||||||
className={`px-4 py-2 rounded-lg whitespace-nowrap transition-colors ${
|
className={`px-4 py-2 rounded-l-lg whitespace-nowrap transition-colors ${
|
||||||
selectedPortfolio?.id === p.id
|
selectedPortfolio?.id === p.id
|
||||||
? 'bg-blue-600 text-white'
|
? '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'
|
: '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}
|
{p.name}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -388,6 +400,12 @@ export default function PortfolioDashboard() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -14,6 +14,11 @@ export interface ActiveSession {
|
|||||||
id: string;
|
id: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
|
deviceType?: string;
|
||||||
|
browser?: string;
|
||||||
|
os?: string;
|
||||||
|
countryCode?: string;
|
||||||
|
city?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
lastActiveAt: string;
|
lastActiveAt: string;
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user