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 };
+}