From b41f417af67f93460e9b76d81f0cf707c483ce27 Mon Sep 17 00:00:00 2001 From: gearnode Date: Tue, 11 Mar 2025 14:33:24 +0100 Subject: [PATCH] Fix unauthenticate user not redirect to login page fix #31 Signed-off-by: gearnode --- apps/console/src/App.tsx | 562 +++++++++--------- apps/console/src/RelayEnvironment.ts | 19 +- apps/console/src/components/ErrorBoundary.tsx | 14 +- apps/console/src/components/NavUser.tsx | 11 +- .../console/src/components/ProtectedRoute.tsx | 20 - apps/console/src/contexts/AuthContext.tsx | 80 --- apps/console/src/pages/LoginPage.tsx | 49 +- apps/console/src/pages/RegisterPage.tsx | 25 +- pkg/server/api/console/v1/resolver.go | 9 +- 9 files changed, 358 insertions(+), 431 deletions(-) delete mode 100644 apps/console/src/components/ProtectedRoute.tsx delete mode 100644 apps/console/src/contexts/AuthContext.tsx diff --git a/apps/console/src/App.tsx b/apps/console/src/App.tsx index 0e74bd728..432e4bcc1 100644 --- a/apps/console/src/App.tsx +++ b/apps/console/src/App.tsx @@ -11,8 +11,6 @@ import ErrorBoundary from "./components/ErrorBoundary"; import ConsoleLayout from "./layouts/ConsoleLayout"; import AuthLayout from "./layouts/AuthLayout"; import { RelayEnvironment } from "./RelayEnvironment"; -import { AuthProvider } from "./contexts/AuthContext"; -import { ProtectedRoute } from "./components/ProtectedRoute"; posthog.init(process.env.POSTHOG_KEY!, { api_host: process.env.POSTHOG_HOST, @@ -63,294 +61,290 @@ function App() { - - - {/* Authentication Routes - Accessible without login */} + + {/* Authentication Routes - Accessible without login */} + + + + } + > - - - } - > - - - - - - } - /> - - - - - - } - > - - - - - - } - /> - - - }> - - - - - - } - /> - - - - - - - } - > - - - - - - } - /> - - - - - - } - > - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - {/* Policy Routes */} - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - + + + } /> - - + + + + + + } + > + + + + + + } + /> + + + + + + + + } + /> + + + + + + + } + > + + + + + + } + /> + + + + + + } + > + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + {/* Policy Routes */} + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + } + /> + diff --git a/apps/console/src/RelayEnvironment.ts b/apps/console/src/RelayEnvironment.ts index c625e7ef7..1995fe1bb 100644 --- a/apps/console/src/RelayEnvironment.ts +++ b/apps/console/src/RelayEnvironment.ts @@ -6,6 +6,17 @@ import { Store, } from "relay-runtime"; import { buildEndpoint } from "./utils"; +import { GraphQLError } from "graphql"; + +export class UnAuthenticatedError extends Error { + constructor() { + super("UNAUTHENTICATED"); + this.name = "UnAuthenticatedError"; + } +} + +const hasUnauthenticatedError = (error: GraphQLError) => + error.extensions?.code == "UNAUTHENTICATED"; const fetchRelay: FetchFunction = async ( request, @@ -66,7 +77,13 @@ const fetchRelay: FetchFunction = async ( const json = await response.json(); - if (Array.isArray(json.errors)) { + if (json.errors) { + const errors = json.errors as GraphQLError[]; + + if (errors.find(hasUnauthenticatedError)) { + throw new UnAuthenticatedError(); + } + throw new Error( `Error fetching GraphQL query '${ request.name diff --git a/apps/console/src/components/ErrorBoundary.tsx b/apps/console/src/components/ErrorBoundary.tsx index 65a2ce4f6..094f3e378 100644 --- a/apps/console/src/components/ErrorBoundary.tsx +++ b/apps/console/src/components/ErrorBoundary.tsx @@ -1,5 +1,7 @@ import { Component, ErrorInfo, ReactNode } from "react"; import { ErrorPage } from "@/pages/ErrorPage"; +import { UnAuthenticatedError } from "@/RelayEnvironment"; +import { Navigate } from "react-router"; interface ErrorBoundaryProps { children: ReactNode; @@ -9,16 +11,20 @@ interface ErrorBoundaryProps { interface ErrorBoundaryState { hasError: boolean; error?: Error; + isUnAuthenticated: boolean; } class ErrorBoundary extends Component { constructor(props: ErrorBoundaryProps) { super(props); - this.state = { hasError: false }; + this.state = { hasError: false, isUnAuthenticated: false }; } static getDerivedStateFromError(error: Error): ErrorBoundaryState { - return { hasError: true, error }; + if (error instanceof UnAuthenticatedError) { + return { hasError: true, error, isUnAuthenticated: true }; + } + return { hasError: true, error, isUnAuthenticated: false }; } componentDidCatch(error: Error, info: ErrorInfo): void { @@ -27,6 +33,10 @@ class ErrorBoundary extends Component { } render(): ReactNode { + if (this.state.isUnAuthenticated) { + return ; + } + if (this.state.hasError) { if (this.props.fallback) { return this.props.fallback; diff --git a/apps/console/src/components/NavUser.tsx b/apps/console/src/components/NavUser.tsx index 8e30dd681..41b1ccf8b 100644 --- a/apps/console/src/components/NavUser.tsx +++ b/apps/console/src/components/NavUser.tsx @@ -27,9 +27,9 @@ import { SidebarMenuItem, useSidebar, } from "@/components/ui/sidebar"; -import { useAuth } from "@/contexts/AuthContext"; import { NavUser_viewer$key } from "./__generated__/NavUser_viewer.graphql"; import { NavUserSkeleton } from "./NavUserSkeleton"; +import { buildEndpoint } from "@/utils"; export const navUserFragment = graphql` fragment NavUser_viewer on User { @@ -42,13 +42,16 @@ export const navUserFragment = graphql` export function NavUser({ viewer }: { viewer: NavUser_viewer$key }) { const { isMobile } = useSidebar(); const currentUser = useFragment(navUserFragment, viewer); - const { logout } = useAuth(); const navigate = useNavigate(); const { organizationId } = useParams(); const handleLogout = async () => { - await logout(); - navigate("/login"); + fetch(buildEndpoint("/auth/logout"), { + method: "POST", + credentials: "include", + }).then(() => { + navigate("/login"); + }); }; if (!organizationId) { diff --git a/apps/console/src/components/ProtectedRoute.tsx b/apps/console/src/components/ProtectedRoute.tsx deleted file mode 100644 index 8d43627f6..000000000 --- a/apps/console/src/components/ProtectedRoute.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Navigate, Outlet, useLocation } from "react-router"; -import { useAuth } from "@/contexts/AuthContext"; - -export function ProtectedRoute() { - const { isAuthenticated, isLoading } = useAuth(); - const location = useLocation(); - - // Show nothing while checking authentication - if (isLoading) { - return null; - } - - // Redirect to login if not authenticated - if (!isAuthenticated) { - return ; - } - - // Render the protected content - return ; -} diff --git a/apps/console/src/contexts/AuthContext.tsx b/apps/console/src/contexts/AuthContext.tsx deleted file mode 100644 index 42b40e109..000000000 --- a/apps/console/src/contexts/AuthContext.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { createContext, useContext, useEffect, useState } from "react"; -import { useNavigate } from "react-router"; -import { buildEndpoint } from "@/utils"; - -interface AuthContextType { - isAuthenticated: boolean; - isLoading: boolean; - checkAuth: () => Promise; - logout: () => Promise; -} - -// TODO: should be remove when backend returns proper authenticated error - -const AuthContext = createContext(undefined); - -export function AuthProvider({ children }: { children: React.ReactNode }) { - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const navigate = useNavigate(); - - const checkAuth = async (): Promise => { - try { - // Make a request to an endpoint that requires authentication - const response = await fetch(buildEndpoint("/api/console/v1/query"), { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - operationName: "CheckAuth", - query: `query CheckAuth { viewer { id } }`, - variables: {}, - }), - }); - - const authenticated = - response.ok && !response.headers.get("WWW-Authenticate"); - setIsAuthenticated(authenticated); - return authenticated; - } catch { - setIsAuthenticated(false); - return false; - } finally { - setIsLoading(false); - } - }; - - const logout = async (): Promise => { - try { - await fetch(buildEndpoint("/console/v1/auth/logout"), { - method: "POST", - credentials: "include", - }); - } finally { - setIsAuthenticated(false); - navigate("/login"); - } - }; - - useEffect(() => { - checkAuth(); - }, []); - - return ( - - {children} - - ); -} - -export function useAuth(): AuthContextType { - const context = useContext(AuthContext); - if (context === undefined) { - throw new Error("useAuth must be used within an AuthProvider"); - } - return context; -} diff --git a/apps/console/src/pages/LoginPage.tsx b/apps/console/src/pages/LoginPage.tsx index 56b9ff594..1a8d53f09 100644 --- a/apps/console/src/pages/LoginPage.tsx +++ b/apps/console/src/pages/LoginPage.tsx @@ -1,50 +1,49 @@ -import { useState } from "react"; -import { Link, useNavigate } from "react-router"; +import { useState, useEffect } from "react"; +import { Link, useLocation } from "react-router"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { buildEndpoint } from "@/utils"; import { useToast } from "@/hooks/use-toast"; -import { useAuth } from "@/contexts/AuthContext"; export default function LoginPage() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); const { toast } = useToast(); - const navigate = useNavigate(); - const { checkAuth } = useAuth(); + const location = useLocation(); + + useEffect(() => { + if (location.state?.authRequired) { + toast({ + title: "Authentication Required", + description: "You need to log in to access this resource", + variant: "destructive", + }); + } + }, [location, toast]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); try { - const response = await fetch(buildEndpoint("/api/console/v1/auth/login"), { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ email, password }), - credentials: "include", - }); + const response = await fetch( + buildEndpoint("/api/console/v1/auth/login"), + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + credentials: "include", + } + ); if (!response.ok) { const error = await response.json(); throw new Error(error.message || "Failed to login"); } - - const authenticated = await checkAuth(); - if (authenticated) { - toast({ - title: "Success", - description: "Logged in successfully", - variant: "default", - }); - navigate("/"); - } else { - throw new Error("Authentication failed"); - } } catch (error: unknown) { toast({ title: "Error", diff --git a/apps/console/src/pages/RegisterPage.tsx b/apps/console/src/pages/RegisterPage.tsx index b0501c3af..125bbd400 100644 --- a/apps/console/src/pages/RegisterPage.tsx +++ b/apps/console/src/pages/RegisterPage.tsx @@ -6,7 +6,6 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { buildEndpoint } from "@/utils"; import { useToast } from "@/hooks/use-toast"; -import { useAuth } from "@/contexts/AuthContext"; export default function RegisterPage() { const [email, setEmail] = useState(""); @@ -15,7 +14,6 @@ export default function RegisterPage() { const [isLoading, setIsLoading] = useState(false); const { toast } = useToast(); const navigate = useNavigate(); - const { checkAuth } = useAuth(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -32,14 +30,17 @@ export default function RegisterPage() { setIsLoading(true); try { - const response = await fetch(buildEndpoint("/api/console/v1/auth/register"), { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - credentials: "include", - body: JSON.stringify({ email, password, fullName }), - }); + const response = await fetch( + buildEndpoint("/api/console/v1/auth/register"), + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify({ email, password, fullName }), + } + ); if (!response.ok) { const errorData = await response.json().catch(() => ({})); @@ -51,10 +52,6 @@ export default function RegisterPage() { description: "Account created successfully", }); - // Check authentication status after registration - await checkAuth(); - - // Redirect to home page after successful registration navigate("/"); } catch (error) { toast({ diff --git a/pkg/server/api/console/v1/resolver.go b/pkg/server/api/console/v1/resolver.go index 1735a36a8..9e14552b9 100644 --- a/pkg/server/api/console/v1/resolver.go +++ b/pkg/server/api/console/v1/resolver.go @@ -110,7 +110,14 @@ func graphqlHandler(proboSvc *probo.Service, usrmgrSvc *usrmgr.Service, authCfg if user == nil { return func(ctx context.Context) *graphql.Response { return &graphql.Response{ - Errors: gqlerror.List{gqlerror.Errorf("authentication required")}, + Errors: gqlerror.List{ + &gqlerror.Error{ + Message: "authentication required", + Extensions: map[string]any{ + "code": "UNAUTHENTICATED", + }, + }, + }, } } }