Skip to content

Commit ac548b6

Browse files
authored
fix: various login issues (#28)
1 parent eb51313 commit ac548b6

7 files changed

Lines changed: 177 additions & 214 deletions

File tree

CLAUDE.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,10 @@ npm run build # Build for production
142142
- Role-based access control
143143

144144
**User Flows**:
145-
1. **New User**: Email → Magic link → Create company → Select app → Redirect
146-
2. **Invited User**: Invitation link → Magic link → Auto-join company → Select app → Redirect
145+
1. **New User**: Email → Magic link → Select app → Redirect
146+
2. **Invited User**: Invitation link → Magic link → Select app → Redirect
147+
148+
> **Note**: Company onboarding has been removed. Users go directly to app selection after authentication.
147149
148150
**Authentication**:
149151
- Magic links expire in 15 minutes
@@ -323,12 +325,13 @@ Both apps include `lovable-tagger` plugin for development mode, supporting AI-as
323325

324326
The Platform app (`apps/platform`) is the central authentication hub for the entire Arda ecosystem:
325327

326-
1. **Entry Point**: Users always start at Platform (port 3002)
328+
1. **Entry Point**: Users always start at Platform (port 5173)
327329
2. **Magic Link Auth**: Passwordless email-based authentication
328-
3. **Company Setup**: Create/join company as originator or investor
329-
4. **App Selection**: Choose Credit App or IDR based on needs
330-
5. **Token Handoff**: Platform passes JWT token to selected app
331-
6. **Cross-App Auth**: Token is valid across all Arda applications
330+
3. **App Selection**: Choose Credit App or IDR immediately after login
331+
4. **Token Handoff**: Platform passes JWT token to selected app
332+
5. **Cross-App Auth**: Token is valid across all Arda applications
333+
334+
> **Simplified Flow**: Company setup has been removed from the onboarding process. Users can access applications immediately after authentication without creating or joining a company.
332335
333336
### Integration Pattern
334337
```typescript

src/auth/DashboardPage.tsx

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { useEffect, useState } from "react";
22
import { useNavigate } from "react-router-dom";
3-
import { Loader2 } from "lucide-react";
3+
import { Loader2, LogOut } from "lucide-react";
44
import { authAPI } from "../shared/lib/api";
55
import { User, CompanyMembership } from "../shared/types";
66
import { CompanySelector } from "../shared/components/company/CompanySelector";
77
import { WorkspaceTabs } from "../shared/components/layout/WorkspaceTabs";
88
import { AppGrid } from "../shared/components/dashboard/AppGrid";
9+
import { Button } from "../shared/components/ui/button";
10+
import { toast } from "sonner";
911

1012
export function DashboardPage() {
1113
console.log('========================================');
@@ -58,7 +60,7 @@ export function DashboardPage() {
5860
timestamp: new Date().toISOString(),
5961
});
6062

61-
// Get full user data with companies
63+
// Get full user data with companies (optional now)
6264
// Try to get user ID from various possible fields
6365
const userId = userResponse.data.id || userResponse.data.user_id || userResponse.data.userId || userResponse.data._id;
6466
console.log("[Dashboard] Attempting to extract user ID", {
@@ -110,46 +112,37 @@ export function DashboardPage() {
110112
})) || [],
111113
};
112114

113-
// Check if user has companies
114-
if (!transformedUser.companies || transformedUser.companies.length === 0) {
115-
console.log("[Dashboard] User has no companies, redirecting to onboarding", {
116-
timestamp: new Date().toISOString(),
117-
});
118-
navigate("/onboarding");
119-
return;
120-
}
121-
122115
setUser(transformedUser);
123116

124-
// Set initial selected company (first one or from localStorage)
125-
const savedCompanyId = localStorage.getItem("selectedCompanyId");
126-
let initialCompany = transformedUser.companies[0];
117+
// Set initial selected company if available (first one or from localStorage)
118+
if (transformedUser.companies && transformedUser.companies.length > 0) {
119+
const savedCompanyId = localStorage.getItem("selectedCompanyId");
120+
let initialCompany = transformedUser.companies[0];
127121

128-
if (savedCompanyId) {
129-
const savedCompany = transformedUser.companies.find((c) => c.companyId === savedCompanyId);
130-
if (savedCompany) {
131-
initialCompany = savedCompany;
122+
if (savedCompanyId) {
123+
const savedCompany = transformedUser.companies.find((c) => c.companyId === savedCompanyId);
124+
if (savedCompany) {
125+
initialCompany = savedCompany;
126+
}
132127
}
133-
}
134128

135-
setSelectedCompany(initialCompany);
129+
setSelectedCompany(initialCompany);
130+
}
136131
} else {
137-
console.warn("[Dashboard] Failed to get full user data, redirecting to onboarding", {
132+
console.warn("[Dashboard] Failed to get full user data, using basic auth data", {
138133
attemptedUserId: userId,
139134
error: fullUserResponse.error,
140135
timestamp: new Date().toISOString(),
141136
});
142-
navigate("/onboarding");
143-
return;
137+
setUser(userResponse.data);
144138
}
145139
} else {
146-
console.warn("[Dashboard] User ID not available, redirecting to onboarding", {
140+
console.warn("[Dashboard] User ID not available, using basic auth data", {
147141
attemptedUserId: userId,
148142
availableFields: Object.keys(userResponse.data),
149143
timestamp: new Date().toISOString(),
150144
});
151-
navigate("/onboarding");
152-
return;
145+
setUser(userResponse.data);
153146
}
154147
} catch (error) {
155148
console.error("[Dashboard] Dashboard initialization error", {
@@ -178,6 +171,36 @@ export function DashboardPage() {
178171
localStorage.setItem("selectedCompanyId", company.companyId);
179172
};
180173

174+
const handleLogout = async () => {
175+
try {
176+
console.log("[Dashboard] Logging out user");
177+
178+
// Call logout API to invalidate session on backend
179+
await authAPI.logout({});
180+
181+
// Clear all auth data from localStorage
182+
localStorage.removeItem("arda_auth_token");
183+
localStorage.removeItem("arda_credit_auth");
184+
localStorage.removeItem("token");
185+
localStorage.removeItem("user_data");
186+
localStorage.removeItem("selectedCompanyId");
187+
188+
toast.success("Logged out successfully");
189+
190+
// Redirect to login page
191+
navigate("/login");
192+
} catch (error) {
193+
console.error("[Dashboard] Logout error:", error);
194+
// Still clear local storage and redirect even if API call fails
195+
localStorage.removeItem("arda_auth_token");
196+
localStorage.removeItem("arda_credit_auth");
197+
localStorage.removeItem("token");
198+
localStorage.removeItem("user_data");
199+
localStorage.removeItem("selectedCompanyId");
200+
navigate("/login");
201+
}
202+
};
203+
181204
if (loading) {
182205
return (
183206
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100">
@@ -189,27 +212,40 @@ export function DashboardPage() {
189212
);
190213
}
191214

192-
if (!user || !selectedCompany) {
215+
if (!user) {
193216
return null;
194217
}
195218

196219
return (
197220
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
198221
<div className="container max-w-6xl mx-auto p-4 py-8 space-y-6">
199-
{/* Header with Company Selector */}
222+
{/* Header with Company Selector (if companies exist) */}
200223
<div className="flex flex-col space-y-4">
201224
<div className="flex items-center justify-between">
202225
<div>
203-
<h1 className="text-3xl font-bold tracking-tight">Welcome back</h1>
226+
<h1 className="text-3xl font-bold tracking-tight">Welcome{user.companies && user.companies.length > 0 ? ' back' : ' to Arda Platform'}</h1>
204227
<p className="text-muted-foreground">
205228
Hello, <strong>{user.email}</strong>
206229
</p>
207230
</div>
208-
<CompanySelector companies={user.companies} selectedCompany={selectedCompany} onCompanyChange={handleCompanyChange} />
231+
<div className="flex items-center gap-4">
232+
{user.companies && user.companies.length > 0 && selectedCompany && (
233+
<CompanySelector companies={user.companies} selectedCompany={selectedCompany} onCompanyChange={handleCompanyChange} />
234+
)}
235+
<Button
236+
variant="outline"
237+
size="sm"
238+
onClick={handleLogout}
239+
className="flex items-center gap-2"
240+
>
241+
<LogOut className="h-4 w-4" />
242+
Logout
243+
</Button>
244+
</div>
209245
</div>
210246

211-
{/* Workspace Tabs */}
212-
<WorkspaceTabs selectedCompany={selectedCompany} user={user} />
247+
{/* Workspace Tabs (only if company is selected) */}
248+
{selectedCompany && <WorkspaceTabs selectedCompany={selectedCompany} user={user} />}
213249
</div>
214250

215251
{/* App Grid */}

src/auth/VerifyPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function VerifyPage() {
4646
}
4747

4848
try {
49-
const response = await authAPI.verifyMagicLink(token, state);
49+
const response = await authAPI.verifyMagicLink({ token, state });
5050
console.log('[VerifyPage] Verify response received:', {
5151
success: response.success,
5252
hasData: !!response.data,

src/shared/components/auth/MagicLinkForm.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Button } from '../ui/button';
77
import { Input } from '../ui/input';
88
import { Label } from '../ui/label';
99
import { Alert, AlertDescription } from '../ui/alert';
10-
import { authAPI } from '../../lib/api';
10+
import { authAPI, userApi } from '../../lib/api';
1111
import { toast } from 'sonner';
1212

1313
const magicLinkSchema = z.object({
@@ -35,9 +35,37 @@ export function MagicLinkForm({ invitationCode }: MagicLinkFormProps) {
3535
const onSubmit = async (data: MagicLinkFormData) => {
3636
setIsLoading(true);
3737
try {
38-
console.log('Sending magic link request:', { email: data.email, invitationCode });
38+
// Step 1: Register the user first if we have an invitation code
39+
if (invitationCode) {
40+
console.log('Registering new user:', { email: data.email, invitationCode });
41+
42+
try {
43+
const registerResponse = await userApi.register({
44+
email: data.email,
45+
invite_code: invitationCode,
46+
});
47+
48+
console.log('User registered successfully:', registerResponse);
49+
toast.success('Account created!', {
50+
description: `Welcome! Sending magic link to ${data.email}`,
51+
});
52+
} catch (registerError: any) {
53+
// If user already exists, that's okay - proceed to magic link
54+
if (registerError.message?.includes('already exists') || registerError.status === 409) {
55+
console.log('User already exists, proceeding to magic link');
56+
} else {
57+
// For other errors, show error and stop
58+
console.error('Registration error:', registerError);
59+
toast.error('Failed to create account', {
60+
description: registerError.message || 'Please try again',
61+
});
62+
return;
63+
}
64+
}
65+
}
3966

40-
// Make actual API call
67+
// Step 2: Send magic link (whether new user or existing)
68+
console.log('Sending magic link request:', { email: data.email, invitationCode });
4169
const response = await authAPI.requestMagicLink({
4270
email: data.email,
4371
invite_code: invitationCode || undefined,

src/shared/components/dashboard/AppGrid.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { CompanyMembership } from "../../types";
66
import { getAppURL, getAuthToken } from "../../lib/utils";
77

88
interface AppGridProps {
9-
selectedCompany: CompanyMembership;
9+
selectedCompany?: CompanyMembership | null;
1010
}
1111

1212
export function AppGrid({ selectedCompany }: AppGridProps) {
@@ -40,12 +40,14 @@ export function AppGrid({ selectedCompany }: AppGridProps) {
4040
const appUrl = getAppURL(appId);
4141
const token = getAuthToken();
4242

43-
// Pass authentication token and company context to the app
43+
// Pass authentication token and company context (if available) to the app
4444
const params = new URLSearchParams();
4545
if (token) {
4646
params.append("token", token);
4747
}
48-
params.append("companyId", selectedCompany.companyId);
48+
if (selectedCompany?.companyId) {
49+
params.append("companyId", selectedCompany.companyId);
50+
}
4951

5052
const separator = appUrl.includes("?") ? "&" : "?";
5153
window.location.href = `${appUrl}${separator}${params.toString()}`;
@@ -56,7 +58,11 @@ export function AppGrid({ selectedCompany }: AppGridProps) {
5658
<div className="text-center space-y-2">
5759
<h2 className="text-2xl font-bold">Available Applications</h2>
5860
<p className="text-muted-foreground">
59-
Choose an application to access with <strong>{selectedCompany.company.name}</strong>
61+
{selectedCompany ? (
62+
<>Choose an application to access with <strong>{selectedCompany.company.name}</strong></>
63+
) : (
64+
<>Choose an application to get started</>
65+
)}
6066
</p>
6167
</div>
6268

@@ -75,7 +81,7 @@ export function AppGrid({ selectedCompany }: AppGridProps) {
7581
</CardTitle>
7682
<CardDescription>{app.description}</CardDescription>
7783
</div>
78-
{selectedCompany.company.type === app.primaryForType && (
84+
{selectedCompany?.company.type === app.primaryForType && (
7985
<Badge variant="secondary" className="text-xs">
8086
Recommended
8187
</Badge>
@@ -104,7 +110,7 @@ export function AppGrid({ selectedCompany }: AppGridProps) {
104110
</Button>
105111
</CardContent>
106112

107-
{selectedCompany.company.type === "originator" && app.primaryForType === "originator" && (
113+
{selectedCompany?.company.type === "originator" && app.primaryForType === "originator" && (
108114
<div className="absolute top-2 right-2">
109115
<Badge className="text-xs">{selectedCompany.company.type}</Badge>
110116
</div>

src/shared/config/runtime.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@ declare global {
2323
export const getConfig = (): RuntimeConfig => {
2424
// Check if we're in production (served over HTTPS) - use relative paths to avoid mixed content
2525
const isProduction = typeof window !== 'undefined' && window.location.protocol === 'https:';
26+
const isDevelopment = import.meta.env?.DEV || import.meta.env?.MODE === 'development';
2627

2728
console.log('🔧 Runtime Config:', {
2829
isProduction,
30+
isDevelopment,
2931
hasWindowEnv: typeof window !== 'undefined' && !!window.__ENV__,
30-
protocol: typeof window !== 'undefined' ? window.location.protocol : 'unknown'
32+
protocol: typeof window !== 'undefined' ? window.location.protocol : 'unknown',
33+
mode: import.meta.env?.MODE
3134
});
3235

3336
if (isProduction) {
@@ -39,21 +42,33 @@ export const getConfig = (): RuntimeConfig => {
3942
};
4043
}
4144

42-
// In development or when window.__ENV__ exists but not HTTPS
45+
// In development mode, use empty SERVER_URL to leverage Vite proxy
46+
// Vite dev server proxies /api/* to the backend server
47+
if (isDevelopment) {
48+
console.log('🔨 Development mode - using Vite proxy for API requests');
49+
console.log('📡 All /api/* requests will be proxied by Vite to the backend');
50+
return {
51+
SERVER_URL: '', // Empty = use Vite proxy at /api/*
52+
CREDIT_APP_URL: import.meta.env.VITE_CREDIT_APP_URL || 'http://localhost:3000',
53+
IDR_URL: import.meta.env.VITE_IDR_URL || 'http://localhost:3001',
54+
};
55+
}
56+
57+
// Staging/preview mode with window.__ENV__
4358
if (typeof window !== 'undefined' && window.__ENV__) {
44-
console.log('🛠️ Using window.__ENV__ configuration for development/staging');
59+
console.log('🛠️ Using window.__ENV__ configuration for staging/preview');
4560
return {
46-
SERVER_URL: window.__ENV__.VITE_SERVER_URL || 'http://localhost:8080',
61+
SERVER_URL: window.__ENV__.VITE_SERVER_URL || '',
4762
CREDIT_APP_URL: window.__ENV__.VITE_CREDIT_APP_URL || 'http://localhost:3000',
4863
IDR_URL: window.__ENV__.VITE_IDR_URL || 'http://localhost:3001',
4964
};
5065
}
5166

52-
// Development fallback - no path prefixes for local dev with pnpm dev
53-
console.log('🔨 Using import.meta.env fallback for local development');
67+
// Fallback for edge cases
68+
console.log('⚠️ Fallback configuration');
5469
return {
55-
SERVER_URL: import.meta.env.VITE_SERVER_URL || 'http://localhost:8080',
56-
CREDIT_APP_URL: import.meta.env.VITE_CREDIT_APP_URL || 'http://localhost:3000',
57-
IDR_URL: import.meta.env.VITE_IDR_URL || 'http://localhost:3001',
70+
SERVER_URL: '',
71+
CREDIT_APP_URL: 'http://localhost:3000',
72+
IDR_URL: 'http://localhost:3001',
5873
};
5974
};

0 commit comments

Comments
 (0)