Wire login flow
Wire up login API integration with real API client and AuthContext; replace mock login with session-based login, connect login page to /api/v1/auth/login, and route post-login to profile. Update App routing to use AppRoutes wrapper and integrate auth context. X-Lovable-Edit-ID: edt-2c1a2967-d81b-4437-a449-5bbbf890b803
This commit is contained in:
+46
-35
@@ -42,44 +42,55 @@ const App = () => (
|
|||||||
<Toaster />
|
<Toaster />
|
||||||
<Sonner />
|
<Sonner />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<AppRoutes />
|
||||||
{/* Index redirect */}
|
|
||||||
<Route path="/" element={<Index />} />
|
|
||||||
|
|
||||||
{/* Public routes */}
|
|
||||||
<Route element={<PublicLayout />}>
|
|
||||||
<Route path="/login" element={<LoginPage />} />
|
|
||||||
<Route path="/register" element={<RegisterPage />} />
|
|
||||||
<Route path="/verify-email" element={<VerifyEmailPage />} />
|
|
||||||
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
|
|
||||||
<Route path="/reset-password" element={<ResetPasswordPage />} />
|
|
||||||
<Route path="/invite" element={<InviteAcceptPage />} />
|
|
||||||
<Route path="/consent" element={<OIDCConsentPage />} />
|
|
||||||
<Route path="/error" element={<OIDCErrorPage />} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* Authenticated routes */}
|
|
||||||
<Route element={<AuthenticatedLayout />}>
|
|
||||||
{/* User routes */}
|
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
|
||||||
<Route path="/security" element={<SecurityPage />} />
|
|
||||||
<Route path="/linked-accounts" element={<LinkedAccountsPage />} />
|
|
||||||
<Route path="/activity" element={<ActivityPage />} />
|
|
||||||
|
|
||||||
{/* Organization routes */}
|
|
||||||
<Route path="/org" element={<OrgOverviewPage />} />
|
|
||||||
<Route path="/org/members" element={<MembersPage />} />
|
|
||||||
<Route path="/org/policies" element={<PoliciesPage />} />
|
|
||||||
<Route path="/org/audit" element={<OrgAuditPage />} />
|
|
||||||
<Route path="/org/clients" element={<OIDCClientsPage />} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* Catch-all */}
|
|
||||||
<Route path="*" element={<NotFound />} />
|
|
||||||
</Routes>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Separate component so AuthProvider can use useNavigate
|
||||||
|
import { AuthProvider } from "@/contexts/AuthContext";
|
||||||
|
|
||||||
|
function AppRoutes() {
|
||||||
|
return (
|
||||||
|
<AuthProvider>
|
||||||
|
<Routes>
|
||||||
|
{/* Index redirect */}
|
||||||
|
<Route path="/" element={<Index />} />
|
||||||
|
|
||||||
|
{/* Public routes */}
|
||||||
|
<Route element={<PublicLayout />}>
|
||||||
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
|
<Route path="/verify-email" element={<VerifyEmailPage />} />
|
||||||
|
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
|
||||||
|
<Route path="/reset-password" element={<ResetPasswordPage />} />
|
||||||
|
<Route path="/invite" element={<InviteAcceptPage />} />
|
||||||
|
<Route path="/consent" element={<OIDCConsentPage />} />
|
||||||
|
<Route path="/error" element={<OIDCErrorPage />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* Authenticated routes */}
|
||||||
|
<Route element={<AuthenticatedLayout />}>
|
||||||
|
{/* User routes */}
|
||||||
|
<Route path="/profile" element={<ProfilePage />} />
|
||||||
|
<Route path="/security" element={<SecurityPage />} />
|
||||||
|
<Route path="/linked-accounts" element={<LinkedAccountsPage />} />
|
||||||
|
<Route path="/activity" element={<ActivityPage />} />
|
||||||
|
|
||||||
|
{/* Organization routes */}
|
||||||
|
<Route path="/org" element={<OrgOverviewPage />} />
|
||||||
|
<Route path="/org/members" element={<MembersPage />} />
|
||||||
|
<Route path="/org/policies" element={<PoliciesPage />} />
|
||||||
|
<Route path="/org/audit" element={<OrgAuditPage />} />
|
||||||
|
<Route path="/org/clients" element={<OIDCClientsPage />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* Catch-all */}
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { api, User, ApiError } from '@/lib/api';
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
user: User | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
login: (email: string, password: string, rememberMe?: boolean) => Promise<void>;
|
||||||
|
logout: () => Promise<void>;
|
||||||
|
refreshUser: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType | null>(null);
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const refreshUser = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.users.me();
|
||||||
|
setUser(response.user);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiError && error.code === 401) {
|
||||||
|
setUser(null);
|
||||||
|
}
|
||||||
|
// Silently fail for other errors during refresh
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Check session on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const checkSession = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.users.me();
|
||||||
|
setUser(response.user);
|
||||||
|
} catch {
|
||||||
|
setUser(null);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkSession();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const login = useCallback(async (email: string, password: string, rememberMe = false) => {
|
||||||
|
const response = await api.auth.login(email, password, rememberMe);
|
||||||
|
setUser(response.user);
|
||||||
|
navigate('/profile');
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
const logout = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await api.auth.logout();
|
||||||
|
} finally {
|
||||||
|
setUser(null);
|
||||||
|
navigate('/login');
|
||||||
|
}
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{
|
||||||
|
user,
|
||||||
|
isLoading,
|
||||||
|
isAuthenticated: !!user,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
refreshUser,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAuth must be used within an AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
// API Client for Gatehouse Backend
|
||||||
|
// Uses session-based authentication with cookies
|
||||||
|
|
||||||
|
const API_BASE = '/api/v1';
|
||||||
|
|
||||||
|
interface ApiResponse<T = unknown> {
|
||||||
|
version: string;
|
||||||
|
success: boolean;
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data?: T;
|
||||||
|
error?: {
|
||||||
|
type: string;
|
||||||
|
details: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
full_name: string | null;
|
||||||
|
avatar_url: string | null;
|
||||||
|
is_active: boolean;
|
||||||
|
is_verified: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
id: string;
|
||||||
|
expires_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
user: User;
|
||||||
|
session: Session;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProfileResponse {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ApiError extends Error {
|
||||||
|
code: number;
|
||||||
|
type: string;
|
||||||
|
details: Record<string, unknown>;
|
||||||
|
|
||||||
|
constructor(message: string, code: number, type: string, details: Record<string, unknown> = {}) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ApiError';
|
||||||
|
this.code = code;
|
||||||
|
this.type = type;
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options: RequestInit = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||||
|
...options,
|
||||||
|
credentials: 'include', // Important: include session cookies
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const json: ApiResponse<T> = await response.json();
|
||||||
|
|
||||||
|
if (!json.success) {
|
||||||
|
throw new ApiError(
|
||||||
|
json.message || 'An error occurred',
|
||||||
|
json.code,
|
||||||
|
json.error?.type || 'UNKNOWN_ERROR',
|
||||||
|
json.error?.details || {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.data as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
auth: {
|
||||||
|
login: (email: string, password: string, remember_me = false) =>
|
||||||
|
request<LoginResponse>('/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email, password, remember_me }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
logout: () =>
|
||||||
|
request<void>('/auth/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
users: {
|
||||||
|
me: () => request<ProfileResponse>('/users/me'),
|
||||||
|
|
||||||
|
updateMe: (data: { full_name?: string; avatar_url?: string }) =>
|
||||||
|
request<ProfileResponse>('/users/me', {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ApiError };
|
||||||
@@ -1,25 +1,42 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Mail, Lock, ArrowRight, Fingerprint } from "lucide-react";
|
import { Mail, Lock, ArrowRight, Fingerprint } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
import { ApiError } from "@/lib/api";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const navigate = useNavigate();
|
const { login } = useAuth();
|
||||||
|
const { toast } = useToast();
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
const [rememberMe, setRememberMe] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
// Mock login - will be replaced with actual auth
|
|
||||||
setTimeout(() => {
|
try {
|
||||||
|
await login(email, password, rememberMe);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof ApiError
|
||||||
|
? error.message
|
||||||
|
: "An unexpected error occurred";
|
||||||
|
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Sign in failed",
|
||||||
|
description: message,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
navigate("/profile");
|
}
|
||||||
}, 1000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,15 +68,7 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<Label htmlFor="password">Password</Label>
|
||||||
<Label htmlFor="password">Password</Label>
|
|
||||||
<Link
|
|
||||||
to="/forgot-password"
|
|
||||||
className="text-sm text-accent hover:underline"
|
|
||||||
>
|
|
||||||
Forgot password?
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
@@ -74,6 +83,25 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="remember"
|
||||||
|
checked={rememberMe}
|
||||||
|
onCheckedChange={(checked) => setRememberMe(checked === true)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="remember" className="text-sm font-normal cursor-pointer">
|
||||||
|
Remember me
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
to="/forgot-password"
|
||||||
|
className="text-sm text-accent hover:underline"
|
||||||
|
>
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
"Signing in..."
|
"Signing in..."
|
||||||
|
|||||||
Reference in New Issue
Block a user