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:
gpt-engineer-app[bot]
2026-01-06 15:33:03 +00:00
4 changed files with 284 additions and 50 deletions
+14 -3
View File
@@ -42,6 +42,18 @@ const App = () => (
<Toaster />
<Sonner />
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
</TooltipProvider>
</QueryClientProvider>
);
// Separate component so AuthProvider can use useNavigate
import { AuthProvider } from "@/contexts/AuthContext";
function AppRoutes() {
return (
<AuthProvider>
<Routes>
{/* Index redirect */}
<Route path="/" element={<Index />} />
@@ -77,9 +89,8 @@ const App = () => (
{/* Catch-all */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</TooltipProvider>
</QueryClientProvider>
</AuthProvider>
);
}
export default App;
+86
View File
@@ -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
View File
@@ -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 };
+42 -14
View File
@@ -1,25 +1,42 @@
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 { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
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() {
const navigate = useNavigate();
const { login } = useAuth();
const { toast } = useToast();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [rememberMe, setRememberMe] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
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);
navigate("/profile");
}, 1000);
}
};
return (
@@ -51,15 +68,7 @@ export default function LoginPage() {
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="password">Password</Label>
<Link
to="/forgot-password"
className="text-sm text-accent hover:underline"
>
Forgot password?
</Link>
</div>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
@@ -74,6 +83,25 @@ export default function LoginPage() {
</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}>
{isLoading ? (
"Signing in..."