diff --git a/sky/dashboard/package-lock.json b/sky/dashboard/package-lock.json index e41998e426f..1bb2986e8d2 100644 --- a/sky/dashboard/package-lock.json +++ b/sky/dashboard/package-lock.json @@ -21,6 +21,7 @@ "chart.js": "^4.4.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cookie": "^1.0.2", "date-fns": "^3.6.0", "express": "^4.19.2", "http-proxy-middleware": "^3.0.0", @@ -6805,12 +6806,12 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" } }, "node_modules/cookie-signature": { @@ -8251,6 +8252,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", diff --git a/sky/dashboard/package.json b/sky/dashboard/package.json index 31dbbf5bfab..9ae19d7f4fe 100644 --- a/sky/dashboard/package.json +++ b/sky/dashboard/package.json @@ -28,6 +28,7 @@ "chart.js": "^4.4.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cookie": "^1.0.2", "date-fns": "^3.6.0", "express": "^4.19.2", "http-proxy-middleware": "^3.0.0", diff --git a/sky/dashboard/src/components/auth/signout.jsx b/sky/dashboard/src/components/auth/signout.jsx new file mode 100644 index 00000000000..f569c837ba3 --- /dev/null +++ b/sky/dashboard/src/components/auth/signout.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { useSignOut } from '@/hooks/auth/useSignOut'; +import Link from 'next/link'; + +export function SignOut(props) { + const { asChild = false, children, ...rest } = props; + const Comp = asChild ? Slot : Link; + + const { signOutUrl } = useSignOut(); + + return ( + + {children} + + ); +} diff --git a/sky/dashboard/src/components/elements/sidebar.jsx b/sky/dashboard/src/components/elements/sidebar.jsx index 3884f87191e..58b50b5c4d5 100755 --- a/sky/dashboard/src/components/elements/sidebar.jsx +++ b/sky/dashboard/src/components/elements/sidebar.jsx @@ -26,6 +26,8 @@ import { Settings, User } from 'lucide-react'; import { BASE_PATH, ENDPOINT } from '@/data/connectors/constants'; import { CustomTooltip } from '@/components/utils'; import { useMobile } from '@/hooks/useMobile'; +import { SignOut } from '../auth/signout'; +import { useAuthMethod } from '@/hooks/auth/useAuthMethod'; // Create a context for sidebar state management const SidebarContext = createContext(null); @@ -161,6 +163,7 @@ export function TopBar() { const { userEmail, userRole, isMobileSidebarOpen, toggleMobileSidebar } = useSidebar(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const authMethod = useAuthMethod(); const dropdownRef = useRef(null); const mobileNavRef = useRef(null); @@ -487,6 +490,11 @@ export function TopBar() { > See all users + {authMethod === 'oauth2' && ( + + Sign out + + )} )} diff --git a/sky/dashboard/src/hooks/auth/useAuthMethod.js b/sky/dashboard/src/hooks/auth/useAuthMethod.js new file mode 100644 index 00000000000..4124dc3f342 --- /dev/null +++ b/sky/dashboard/src/hooks/auth/useAuthMethod.js @@ -0,0 +1,13 @@ +import { useCallback } from 'react'; +import * as cookie from 'cookie'; + +export function useAuthMethod() { + const getBrowserCookie = useCallback((name) => { + const cookies = cookie.parse(document.cookie || ''); + return name in cookies ? cookies[name] : undefined; + }, []); + + // Only check OAuth2 + // Cannot check other methods (e.g., basic auth) from the browser + return getBrowserCookie('_oauth2_proxy') ? 'oauth2' : undefined; +} diff --git a/sky/dashboard/src/hooks/auth/useSignOut.js b/sky/dashboard/src/hooks/auth/useSignOut.js new file mode 100644 index 00000000000..800d0037d9f --- /dev/null +++ b/sky/dashboard/src/hooks/auth/useSignOut.js @@ -0,0 +1,27 @@ +import { useMemo } from 'react'; +import { useAuthMethod } from './useAuthMethod'; +import { useRouter } from 'next/router'; +import { ENDPOINT } from '@/data/connectors/constants'; + +export function useSignOut(props) { + const { redirect } = props || {}; + + const authMethod = useAuthMethod(); + const router = useRouter(); + + const signOutUrl = useMemo(() => { + if (authMethod === 'oauth2') { + const url = new URL(`${ENDPOINT}/oauth2/sign_out`, window.location.href); + + if (redirect) { + url.searchParams.append('rd', redirect); + } + + return url; + } else { + return undefined; + } + }, [authMethod, router, redirect]); + + return { signOutUrl }; +}