Skip to content

Commit a7fdf17

Browse files
committed
feat: implement Supabase authentication system with protected routes
1 parent 4e63d41 commit a7fdf17

File tree

3 files changed

+211
-0
lines changed

3 files changed

+211
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
import { Navigate } from 'react-router-dom';
3+
import { useAuth } from '../../context/AuthContext';
4+
5+
const ProtectedRoute = ({ children }) => {
6+
const { user, loading } = useAuth();
7+
8+
if (loading) {
9+
return (
10+
<div className="min-h-screen flex items-center justify-center">
11+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
12+
</div>
13+
);
14+
}
15+
16+
if (!user) {
17+
return <Navigate to="/" replace />;
18+
}
19+
20+
return children;
21+
};
22+
23+
export default ProtectedRoute;

src/context/AuthContext.jsx

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React, { createContext, useContext, useEffect, useState } from 'react';
2+
import { supabase } from '../config/supabase';
3+
4+
const AuthContext = createContext({});
5+
6+
export const useAuth = () => {
7+
const context = useContext(AuthContext);
8+
if (!context) {
9+
throw new Error('useAuth must be used within an AuthProvider');
10+
}
11+
return context;
12+
};
13+
14+
export const AuthProvider = ({ children }) => {
15+
const [user, setUser] = useState(null);
16+
const [loading, setLoading] = useState(true);
17+
18+
useEffect(() => {
19+
// Get initial session
20+
const getSession = async () => {
21+
const { data: { session } } = await supabase.auth.getSession();
22+
setUser(session?.user ?? null);
23+
setLoading(false);
24+
};
25+
26+
getSession();
27+
28+
// Listen for auth changes
29+
const { data: { subscription } } = supabase.auth.onAuthStateChange(
30+
async (event, session) => {
31+
setUser(session?.user ?? null);
32+
setLoading(false);
33+
}
34+
);
35+
36+
return () => subscription.unsubscribe();
37+
}, []);
38+
39+
const signUp = async (email, password, name) => {
40+
try {
41+
const { data, error } = await supabase.auth.signUp({
42+
email,
43+
password,
44+
options: {
45+
data: {
46+
full_name: name,
47+
},
48+
},
49+
});
50+
51+
if (error) throw error;
52+
return { data, error: null };
53+
} catch (error) {
54+
return { data: null, error };
55+
}
56+
};
57+
58+
const signIn = async (email, password) => {
59+
try {
60+
const { data, error } = await supabase.auth.signInWithPassword({
61+
email,
62+
password,
63+
});
64+
65+
if (error) throw error;
66+
return { data, error: null };
67+
} catch (error) {
68+
return { data: null, error };
69+
}
70+
};
71+
72+
const signOut = async () => {
73+
try {
74+
const { error } = await supabase.auth.signOut();
75+
if (error) throw error;
76+
return { error: null };
77+
} catch (error) {
78+
return { error };
79+
}
80+
};
81+
82+
const resetPassword = async (email) => {
83+
try {
84+
const { error } = await supabase.auth.resetPasswordForEmail(email, {
85+
redirectTo: `${window.location.origin}/reset-password`,
86+
});
87+
if (error) throw error;
88+
return { error: null };
89+
} catch (error) {
90+
return { error };
91+
}
92+
};
93+
94+
const updateProfile = async (updates) => {
95+
try {
96+
const { data, error } = await supabase.auth.updateUser({
97+
data: updates,
98+
});
99+
if (error) throw error;
100+
return { data, error: null };
101+
} catch (error) {
102+
return { data: null, error };
103+
}
104+
};
105+
106+
const value = {
107+
user,
108+
loading,
109+
signUp,
110+
signIn,
111+
signOut,
112+
resetPassword,
113+
updateProfile,
114+
};
115+
116+
return (
117+
<AuthContext.Provider value={value}>
118+
{children}
119+
</AuthContext.Provider>
120+
);
121+
};

src/services/api.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import axios from 'axios';
2+
import { supabase } from '../config/supabase';
3+
4+
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://portfolio-tracker-backend-y7ne.onrender.com/api';
5+
6+
// Create axios instance with base URL
7+
const api = axios.create({
8+
baseURL: API_BASE_URL,
9+
timeout: 10000
10+
});
11+
12+
// Add request interceptor to include auth token
13+
api.interceptors.request.use(
14+
async (config) => {
15+
try {
16+
// Get the current session
17+
const { data: { session } } = await supabase.auth.getSession();
18+
19+
console.log('🔍 API Request Debug:', {
20+
url: config.url,
21+
method: config.method,
22+
hasSession: !!session,
23+
hasToken: !!session?.access_token,
24+
tokenPreview: session?.access_token ? `${session.access_token.substring(0, 20)}...` : 'No token'
25+
});
26+
27+
if (session?.access_token) {
28+
config.headers.Authorization = `Bearer ${session.access_token}`;
29+
console.log('✅ Auth token added to request');
30+
} else {
31+
console.log('❌ No auth token available');
32+
}
33+
} catch (error) {
34+
console.error('❌ Error getting auth token:', error);
35+
}
36+
37+
return config;
38+
},
39+
(error) => {
40+
console.error('❌ Request interceptor error:', error);
41+
return Promise.reject(error);
42+
}
43+
);
44+
45+
// Add response interceptor to handle auth errors
46+
api.interceptors.response.use(
47+
(response) => {
48+
console.log('✅ API Response success:', response.config.url);
49+
return response;
50+
},
51+
async (error) => {
52+
console.error('❌ API Response error:', {
53+
url: error.config?.url,
54+
status: error.response?.status,
55+
message: error.response?.data?.error || error.message
56+
});
57+
58+
if (error.response?.status === 401) {
59+
// Token expired or invalid, redirect to login
60+
console.log('🔐 Authentication failed, redirecting to login...');
61+
window.location.href = '/';
62+
}
63+
return Promise.reject(error);
64+
}
65+
);
66+
67+
export default api;

0 commit comments

Comments
 (0)