-
Notifications
You must be signed in to change notification settings - Fork 134
feat: Added a Basic Authentication System #123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
98913de
b3900c6
97fa482
2638405
1099786
850c476
d0d8329
6cd2af7
a1346cb
c43c775
fbe610a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| syntax-highlighting=True | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| VITE_SUPABASE_URL=YOUR SUPABASE URL | ||
| VITE_BASE_URL = http://localhost:5173/ | ||
| VITE_SUPABASE_KEY=YOUR SUPABASE ANON KEY |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,6 +1,8 @@ | ||||||
| import React, { useEffect, useState } from 'react'; | ||||||
| import { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from 'react-router-dom'; | ||||||
| import { Toaster } from 'react-hot-toast'; | ||||||
| import { AnimatePresence } from 'framer-motion'; | ||||||
| import toast, { Toaster } from 'react-hot-toast'; | ||||||
|
|
||||||
| import Sidebar from './components/layout/Sidebar'; | ||||||
| import Dashboard from './components/dashboard/Dashboard'; | ||||||
| import BotIntegrationPage from './components/integration/BotIntegrationPage'; | ||||||
|
|
@@ -12,29 +14,73 @@ import SupportPage from './components/pages/SupportPage'; | |||||
| import LandingPage from './components/landing/LandingPage'; | ||||||
| import LoginPage from './components/pages/LoginPage'; | ||||||
| import ProfilePage from './components/pages/ProfilePage'; | ||||||
| import { AnimatePresence } from 'framer-motion'; | ||||||
| import SignUpPage from './components/pages/SignUpPage'; | ||||||
| import { supabase } from './lib/supabaseClient'; | ||||||
| import ForgotPasswrdPage from './components/pages/ForgotPasswrdPage'; | ||||||
| import ResetPasswordPage from './components/pages/ResetPasswordPage'; | ||||||
|
|
||||||
| function App() { | ||||||
| const [isSidebarOpen, setIsSidebarOpen] = useState(true); | ||||||
| const [repoData, setRepoData] = useState<any>(null); // Store fetched repo stats | ||||||
| const [repoData, setRepoData] = useState<any>(null); | ||||||
| const [isAuthenticated, setIsAuthenticated] = useState(false); | ||||||
|
|
||||||
| // Check for existing authentication on app load | ||||||
| // Auto login if user has already logged in | ||||||
| useEffect(() => { | ||||||
| const savedAuth = localStorage.getItem('isAuthenticated'); | ||||||
| if (savedAuth === 'true') { | ||||||
| setIsAuthenticated(true); | ||||||
| } | ||||||
| supabase.auth.getSession().then(({ data, error }) => { | ||||||
| if (error) { | ||||||
| toast.error('User Login Failed'); | ||||||
| console.error('Error checking session:', error); | ||||||
| return; | ||||||
| } | ||||||
| setIsAuthenticated(!!data.session); | ||||||
| }); | ||||||
|
|
||||||
| const { data: subscription } = supabase.auth.onAuthStateChange( | ||||||
| (event, session) => { | ||||||
| console.log("Auth event:", event, session); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not log the full Access/refresh tokens can end up in browser logs. Log the event only (or at most - console.log("Auth event:", event, session);
+ console.log("Auth event:", event);π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||
| switch (event) { | ||||||
| case "SIGNED_IN": | ||||||
| setIsAuthenticated(true); | ||||||
| toast.success("Signed in!"); | ||||||
| break; | ||||||
|
|
||||||
| case "SIGNED_OUT": | ||||||
| setIsAuthenticated(false); | ||||||
| setRepoData(null); | ||||||
| toast.success("Signed out!"); | ||||||
| break; | ||||||
|
|
||||||
| case "PASSWORD_RECOVERY": | ||||||
| toast("Check your email to reset your password."); | ||||||
| break; | ||||||
| case "TOKEN_REFRESHED": | ||||||
| console.log("Session refreshed"); | ||||||
| break; | ||||||
| case "USER_UPDATED": | ||||||
| console.log("User updated", session?.user); | ||||||
| break; | ||||||
| } | ||||||
| } | ||||||
| ); | ||||||
|
|
||||||
| return () => { | ||||||
| subscription.subscription.unsubscribe(); | ||||||
| }; | ||||||
| }, []); | ||||||
|
|
||||||
| const handleLogin = () => { | ||||||
| setIsAuthenticated(true); | ||||||
| localStorage.setItem('isAuthenticated', 'true'); | ||||||
| }; | ||||||
|
|
||||||
| const handleLogout = () => { | ||||||
| const handleLogout = async () => { | ||||||
| const { error } = await supabase.auth.signOut(); | ||||||
| if (error) { | ||||||
| toast.error('Logout failed'); | ||||||
| console.error('Error during logout:', error); | ||||||
| return; | ||||||
| } | ||||||
| toast.success('Signed out!'); | ||||||
| setIsAuthenticated(false); | ||||||
| localStorage.removeItem('isAuthenticated'); | ||||||
| setRepoData(null); | ||||||
| }; | ||||||
|
|
||||||
|
|
@@ -74,6 +120,23 @@ function App() { | |||||
| ) | ||||||
| } | ||||||
| /> | ||||||
| <Route | ||||||
| path="/forgot-password" | ||||||
| element={ | ||||||
| isAuthenticated ? ( | ||||||
| <Navigate to="/" replace /> | ||||||
| ) : ( | ||||||
| <ForgotPasswrdPage /> | ||||||
| ) | ||||||
| } | ||||||
| /> | ||||||
| <Route path="/reset-password" element={<ResetPasswordPage />} /> | ||||||
| <Route | ||||||
| path="/signup" | ||||||
| element={ | ||||||
| isAuthenticated ? <Navigate to="/" replace /> : <SignUpPage /> | ||||||
| } | ||||||
| /> | ||||||
| <Route | ||||||
| path="/" | ||||||
| element={ | ||||||
|
|
@@ -97,4 +160,3 @@ function App() { | |||||
| } | ||||||
|
|
||||||
| export default App; | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,16 +2,16 @@ import React, { useState } from 'react'; | |
| import { motion } from 'framer-motion'; | ||
| import axios from 'axios'; | ||
| import { toast } from 'react-hot-toast'; | ||
| import { useNavigate } from 'react-router-dom'; | ||
|
|
||
| interface Props { | ||
| setRepoData: (data: any) => void; // Function to pass data to parent | ||
| setActivePage: (page: string) => void; // Function to navigate to other pages | ||
| } | ||
|
|
||
| const LandingPage: React.FC<Props> = ({ setRepoData, setActivePage }) => { | ||
| const LandingPage: React.FC<Props> = ({ setRepoData }) => { | ||
| const [repoUrl, setRepoUrl] = useState(''); | ||
| const [loading, setLoading] = useState(false); | ||
|
|
||
| const navigate = useNavigate(); | ||
| const fetchRepoStats = async () => { | ||
| if (!repoUrl) { | ||
| toast.error('Please enter a valid GitHub repository URL.'); | ||
|
|
@@ -23,7 +23,7 @@ const LandingPage: React.FC<Props> = ({ setRepoData, setActivePage }) => { | |
| const response = await axios.post('http://localhost:8000/api/repo-stats', { repo_url: repoUrl }); | ||
| setRepoData(response.data); // Pass fetched data to parent | ||
| toast.success('Repository stats fetched successfully!'); | ||
| setActivePage('dashboard'); // Navigate to dashboard | ||
| navigate('/dashboard'); // Navigate to dashboard | ||
| } catch (error) { | ||
| toast.error('Failed to fetch repository stats. Please check the URL or backend server.'); | ||
| } finally { | ||
|
Comment on lines
23
to
29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion Pass auth token, add timeout, and route 401s to login Improves security, resilience, and UX. - const response = await axios.post('http://localhost:8000/api/repo-stats', { repo_url: repoUrl });
+ const { data: { session } } = await supabase.auth.getSession();
+ const response = await axios.post(
+ `${API_BASE}/api/repo-stats`,
+ { repo_url: repoUrl },
+ {
+ headers: session?.access_token ? { Authorization: `Bearer ${session.access_token}` } : undefined,
+ timeout: 15000,
+ }
+ );
setRepoData(response.data); // Pass fetched data to parent
toast.success('Repository stats fetched successfully!');
navigate('/dashboard'); // Navigate to dashboard
- } catch (error) {
- toast.error('Failed to fetch repository stats. Please check the URL or backend server.');
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ if (error.response?.status === 401) {
+ toast.error('Please log in to analyze repositories.');
+ navigate('/login');
+ return;
+ }
+ const msg = error.response?.data?.message ?? error.message;
+ toast.error(`Failed to fetch repository stats: ${msg}`);
+ } else {
+ toast.error('Failed to fetch repository stats. Please check the URL or backend server.');
+ }
} finally {
setLoading(false);
}
π€ Prompt for AI Agents |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| import { useState, ReactNode, FormEvent } from "react"; | ||
| import { motion } from "framer-motion"; | ||
| import { useNavigate } from 'react-router-dom'; | ||
| import { toast} from "react-hot-toast"; | ||
| import { supabase } from "../../lib/supabaseClient"; | ||
|
|
||
|
|
||
| import { | ||
| Settings, | ||
| Mail, | ||
| Lock, | ||
| } from 'lucide-react'; | ||
|
|
||
| interface AuthLayoutProps { | ||
| children: ReactNode; | ||
| } | ||
|
|
||
| interface InputFieldProps extends React.InputHTMLAttributes<HTMLInputElement> { | ||
| icon: React.ElementType; | ||
| } | ||
|
|
||
|
|
||
| const AuthLayout = ({ children }: AuthLayoutProps) => ( | ||
| <div className="min-h-screen bg-gray-950 flex items-center justify-center p-4"> | ||
| <motion.div | ||
| initial={{ opacity: 0, y: 20 }} | ||
| animate={{ opacity: 1, y: 0 }} | ||
| exit={{ opacity: 0, y: -20 }} | ||
| className="w-full max-w-md" | ||
| > | ||
| {children} | ||
| </motion.div> | ||
| </div> | ||
| ); | ||
|
|
||
| const InputField = ({ icon: Icon, ...props }: InputFieldProps) => ( | ||
| <div className="relative"> | ||
| <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> | ||
| <Icon className="h-5 w-5 text-gray-400" /> | ||
| </div> | ||
| <input | ||
| {...props} | ||
| className="block w-full pl-10 pr-3 py-2 border border-gray-800 rounded-lg bg-gray-900 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent" | ||
| /> | ||
| </div> | ||
| ); | ||
|
|
||
|
|
||
| export default function ForgotPasswrdPage() { | ||
| const navigate = useNavigate(); | ||
| const [isLoading, setIsLoading] = useState<boolean>(false); | ||
| const [mailPage, setMailPage] = useState<boolean>(false); | ||
|
|
||
| const handleAuth = async (e: FormEvent<HTMLFormElement>) => { | ||
| e.preventDefault(); | ||
| const formData = new FormData(e.currentTarget); | ||
| const email = String(formData.get('email') || '').trim(); | ||
| if (!email) { | ||
| toast.error("Please enter your email address."); | ||
| return; | ||
| } | ||
| setIsLoading(true); | ||
| try { | ||
| const base = import.meta.env.VITE_BASE_URL || window.location.origin; | ||
| const redirectTo = new URL('/reset-password', base).toString(); | ||
| const { error } = await supabase.auth.resetPasswordForEmail(email, { redirectTo }); | ||
| if (error) { | ||
| console.error('resetPasswordForEmail failed', error); | ||
| } | ||
| setMailPage(true); | ||
| } catch (err) { | ||
| console.error('resetPasswordForEmail unexpected error', err); | ||
| toast.error("Something went wrong. Please try again."); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <AuthLayout> | ||
|
|
||
| <div className="bg-gray-900 p-8 rounded-xl border border-gray-800"> | ||
| {!mailPage ? ( | ||
| <> | ||
| <motion.div | ||
| initial={{ opacity: 0 }} | ||
| animate={{ opacity: 1 }} | ||
| className="text-center mb-8" | ||
| > | ||
| <h1 className="text-3xl font-bold text-white mb-2">Account Recovery</h1> | ||
| <p className="text-gray-400">Reset your password</p> | ||
| </motion.div> | ||
| <form onSubmit={handleAuth} className="space-y-6"> | ||
| <InputField | ||
| icon={Mail} | ||
| type="email" | ||
| name="email" | ||
| className="mb-7" | ||
| placeholder="Email address" | ||
| required | ||
| /> | ||
| <motion.button | ||
| whileHover={{ scale: 1.02 }} | ||
| whileTap={{ scale: 0.98 }} | ||
| type="submit" | ||
| disabled={isLoading} | ||
| className="w-full py-3 bg-green-500 hover:bg-green-600 text-white rounded-lg font-medium transition-colors flex items-center justify-center" | ||
| > | ||
| {isLoading ? ( | ||
| <motion.div | ||
| animate={{ rotate: 360 }} | ||
| transition={{ duration: 1, repeat: Infinity, ease: "linear" }} | ||
| > | ||
| <Settings size={20} /> | ||
| </motion.div> | ||
| ) : ( | ||
| <> | ||
| <Lock size={20} className="mr-2" /> | ||
| Reset Password | ||
| </> | ||
| )} | ||
| </motion.button> | ||
| <p className="text-center text-gray-400 text-sm"> | ||
| <button | ||
| type="button" | ||
| onClick={() => navigate('/login')} | ||
| className="text-gray-400 hover:text-gray-300 font-medium" | ||
| > | ||
| Back to Sign In | ||
| </button> | ||
| </p> | ||
| </form> | ||
| </> | ||
| ) : ( | ||
| <div className="flex flex-col items-center justify-center py-12"> | ||
| <Mail className="w-16 h-16 text-green-500 mb-4" /> | ||
| <h2 className="text-2xl font-bold text-white mb-2">Check your inbox</h2> | ||
| <p className="text-gray-400 mb-4 text-center"> | ||
| We've sent a password reset email to this address. Please check your inbox and follow the instructions to reset your password. | ||
| </p> | ||
| <button | ||
| type="button" | ||
| onClick={() => { setMailPage(false); navigate('/login') }} | ||
| className="mt-4 px-6 py-2 bg-green-500 hover:bg-green-600 text-white rounded-lg font-medium transition-colors" | ||
| > | ||
| Back to Sign In | ||
| </button> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </AuthLayout> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π οΈ Refactor suggestion
Simplify subscription handling and make cleanup safe.
Current cleanup may throw if
subscriptionis undefined; also double βsubscription.subscriptionβ is awkward.Also applies to: 66-68
π€ Prompt for AI Agents