Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions app/(auth)/auth/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useRouter } from "next/navigation"
import { useTranslation } from "react-i18next"
import { LoginForm, type LoginMethod } from "@/components/auth/login-form"
import { useAuth } from "@/contexts/auth-context"
import { useFirstAccessibleDashboardRoute } from "@/hooks/use-first-accessible-dashboard-route"
import { useMessage } from "@/lib/feedback/message"
import { configManager } from "@/lib/config"
import { fetchOidcProviders, initiateOidcLogin } from "@/lib/oidc"
Expand All @@ -24,6 +25,7 @@ function LoginPageContent() {
const searchParams = useSearchParams()
const message = useMessage()
const { login, isAuthenticated } = useAuth()
const { route: firstAccessibleRoute, isReady: hasResolvedFirstRoute } = useFirstAccessibleDashboardRoute()
const { t } = useTranslation()

const [method, setMethod] = useState<LoginMethod>("accessKeyAndSecretKey")
Expand All @@ -39,10 +41,9 @@ function LoginPageContent() {
const [oidcProviders, setOidcProviders] = useState<OidcProvider[]>([])

useEffect(() => {
if (isAuthenticated) {
router.replace("/browser")
}
}, [isAuthenticated, router])
if (!isAuthenticated || !hasResolvedFirstRoute || !firstAccessibleRoute) return
router.replace(firstAccessibleRoute)
}, [isAuthenticated, hasResolvedFirstRoute, firstAccessibleRoute, router])

useEffect(() => {
if (searchParams.get("unauthorized") === "true") {
Expand Down Expand Up @@ -70,7 +71,6 @@ function LoginPageContent() {
await login(credentials, currentConfig)

message.success(t("Login Success"))
router.replace("/browser")
} catch {
message.error(t("Login Failed"))
}
Expand Down
17 changes: 13 additions & 4 deletions app/(auth)/auth/oidc-callback/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useEffect, useRef, useState } from "react"
import { useRouter } from "next/navigation"
import { useAuth } from "@/contexts/auth-context"
import { useFirstAccessibleDashboardRoute } from "@/hooks/use-first-accessible-dashboard-route"
import { useMessage } from "@/lib/feedback/message"
import { parseOidcCallback } from "@/lib/oidc"
import { isSafeRedirectPath } from "@/lib/routes"
Expand All @@ -11,11 +12,12 @@ import { useTranslation } from "react-i18next"
export default function OidcCallbackPage() {
const router = useRouter()
const { loginWithStsCredentials, isAuthenticated } = useAuth()
const { route: firstAccessibleRoute, isReady: hasResolvedFirstRoute } = useFirstAccessibleDashboardRoute()
const message = useMessage()
const { t } = useTranslation()
const processed = useRef(false)
const [credentialsSet, setCredentialsSet] = useState(false)
const redirectPath = useRef("/browser")
const redirectPath = useRef("/")

// Step 1: Parse hash and store credentials
useEffect(() => {
Expand All @@ -31,7 +33,7 @@ export default function OidcCallbackPage() {
return
}

redirectPath.current = isSafeRedirectPath(credentials.redirect, "/browser")
redirectPath.current = isSafeRedirectPath(credentials.redirect, "/")

loginWithStsCredentials({
AccessKeyId: credentials.accessKey,
Expand All @@ -51,10 +53,17 @@ export default function OidcCallbackPage() {

// Step 2: Wait for auth state to update before navigating
useEffect(() => {
if (credentialsSet && isAuthenticated) {
if (!credentialsSet || !isAuthenticated) return

if (redirectPath.current !== "/") {
router.replace(redirectPath.current)
return
}

if (hasResolvedFirstRoute && firstAccessibleRoute) {
router.replace(firstAccessibleRoute)
}
}, [credentialsSet, isAuthenticated, router])
}, [credentialsSet, isAuthenticated, hasResolvedFirstRoute, firstAccessibleRoute, router])

return (
<div className="flex min-h-screen items-center justify-center bg-gray-100 dark:bg-neutral-800">
Expand Down
16 changes: 14 additions & 2 deletions app/(dashboard)/403/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@ import { useRouter } from "next/navigation"
import { useTranslation } from "react-i18next"
import { Empty, EmptyContent, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from "@/components/ui/empty"
import { Button } from "@/components/ui/button"
import { useAuth } from "@/contexts/auth-context"
import { useFirstAccessibleDashboardRoute } from "@/hooks/use-first-accessible-dashboard-route"
import { DASHBOARD_ROUTE_FALLBACK } from "@/lib/dashboard-route-meta"

export default function ForbiddenPage() {
const { t } = useTranslation()
const router = useRouter()
const { logoutAndRedirect } = useAuth()
const { route } = useFirstAccessibleDashboardRoute()
const fallbackNormalized = DASHBOARD_ROUTE_FALLBACK.replace(/\/+$/, "")
const routeNormalized = route?.replace(/\/+$/, "")
const hasSafeHomeRoute = Boolean(routeNormalized && routeNormalized !== fallbackNormalized)

const handleBack = () => {
router.replace("/browser")
if (!hasSafeHomeRoute || !route) {
logoutAndRedirect()
return
}
router.replace(route)
}

return (
Expand Down Expand Up @@ -40,7 +52,7 @@ export default function ForbiddenPage() {
</EmptyDescription>
</EmptyHeader>
<Button variant="outline" className="mt-6" onClick={handleBack}>
{t("Back to Home")}
{hasSafeHomeRoute ? t("Back to Home") : t("Back to Login")}
</Button>
</EmptyContent>
</Empty>
Expand Down
16 changes: 14 additions & 2 deletions app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { redirect } from "next/navigation"
"use client"

import { useEffect } from "react"
import { useRouter } from "next/navigation"
import { useFirstAccessibleDashboardRoute } from "@/hooks/use-first-accessible-dashboard-route"

export default function HomePage() {
redirect("/browser")
const router = useRouter()
const { route, isReady } = useFirstAccessibleDashboardRoute()

useEffect(() => {
if (!isReady || !route) return
router.replace(route)
}, [isReady, route, router])

return null
}
9 changes: 6 additions & 3 deletions app/(dashboard)/status/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { usePermissions } from "@/hooks/use-permissions"
import { PerformanceSummaryCards } from "../_components/performance-summary-cards"
import { PerformanceUsageCard } from "../_components/performance-usage-card"
import { PerformanceInfrastructureCard } from "../_components/performance-infrastructure-card"
Expand All @@ -29,7 +30,9 @@ dayjs.extend(relativeTime)

export default function PerformancePage() {
const { t } = useTranslation()
const { canAccessPath } = usePermissions()
const { systemInfo, metricsInfo, datausageinfo, storageinfo, loading, error, refetch } = usePerformanceData()
const browserHref = canAccessPath("/browser") ? "/browser" : undefined

const numberFormatter = useMemo(() => new Intl.NumberFormat(), [])

Expand Down Expand Up @@ -58,14 +61,14 @@ export default function PerformancePage() {
display: numberFormatter.format(systemInfo?.buckets?.count ?? 0),
icon: RiArchiveLine,
caption: null as string | null,
href: "/browser",
href: browserHref,
},
{
label: t("Objects"),
display: numberFormatter.format(systemInfo?.objects?.count ?? 0),
icon: RiStackLine,
caption: null as string | null,
href: "/browser",
href: browserHref,
},
{
label: t("Total Capacity"),
Expand All @@ -77,7 +80,7 @@ export default function PerformancePage() {
href: undefined as string | undefined,
},
],
[systemInfo, datausageinfo, numberFormatter, t],
[systemInfo, datausageinfo, numberFormatter, t, browserHref],
)

const fromLastStartTime = useMemo(() => {
Expand Down
46 changes: 33 additions & 13 deletions components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { getIconComponent } from "@/lib/icon-map"
import navs from "@/config/navs"
import type { NavItem } from "@/types/app-config"
import { usePermissions } from "@/hooks/use-permissions"
import { useFirstAccessibleDashboardRoute } from "@/hooks/use-first-accessible-dashboard-route"
import { canAccessDashboardRoute } from "@/lib/dashboard-route-meta"
import { SidebarVersion } from "@/components/sidebars/version"
import { useDirection } from "@/components/ui/direction"
import { getThemeManifest } from "@/lib/theme/manifest"
Expand Down Expand Up @@ -65,23 +67,29 @@ export function AppSidebar() {
const dir = useDirection()
const brandInitial = APP_NAME.charAt(0).toUpperCase() ?? "R"

const { isAdmin, canAccessPath } = usePermissions()
const { isAdmin, canAccessPath, hasResolvedAdmin, hasFetchedPolicy, isLoading } = usePermissions()
const { route: homeRoute } = useFirstAccessibleDashboardRoute()
const isPermissionsReady = hasResolvedAdmin && (isAdmin || (!isLoading && hasFetchedPolicy))

const navGroups: NavItem[][] = []
let current: NavItem[] = []
if (!isPermissionsReady) {
return null
}

const visibleNavs = navs.flatMap((nav) => {
if (nav.type === "divider") {
return [nav]
}

for (const nav of navs) {
let visibleChildren: NavItem[] = []
if (nav.children?.length) {
visibleChildren = nav.children.filter((child) => {
if (child.to && !canAccessPath(child.to)) return false
if (child.isAdminOnly && !isAdmin && !child.to) return false
if (!child.to || isExternal(child)) return true
if (!canAccessDashboardRoute(child.to, { isAdmin, canAccessPath })) return false
return true
})
if (visibleChildren.length === 0 && !nav.to) continue
if (visibleChildren.length === 0 && !nav.to) return []
} else {
if (nav.to && !canAccessPath(nav.to)) continue
if (!nav.to && nav.isAdminOnly && !isAdmin) continue
if (nav.to && !isExternal(nav) && !canAccessDashboardRoute(nav.to, { isAdmin, canAccessPath })) return []
}

const navItem = { ...nav }
Expand All @@ -91,18 +99,30 @@ export function AppSidebar() {
delete navItem.children
}

return [navItem]
})

const navGroups: NavItem[][] = []
let current: NavItem[] = []

for (let index = 0; index < visibleNavs.length; index++) {
const nav = visibleNavs[index]
if (!nav) continue

if (nav.type === "divider") {
if (current.length) {
const hasItemsBefore = current.length > 0
const hasItemsAfter = visibleNavs.slice(index + 1).some((item) => item.type !== "divider")
if (hasItemsBefore && hasItemsAfter) {
navGroups.push(current)
current = []
}
continue
}

current.push(navItem)
current.push(nav)
}

if (current.length) {
if (current.length > 0) {
navGroups.push(current)
}

Expand All @@ -127,7 +147,7 @@ export function AppSidebar() {
className="**:data-[sidebar=menu-button]:text-start! **:data-[sidebar=menu-sub-button]:text-start!"
>
<SidebarHeader>
<Link href="/" className="flex items-center gap-3">
<Link href={homeRoute ?? "/"} className="flex items-center gap-3">
{isCollapsed ? (
<div className="flex size-8 items-center justify-center rounded-lg bg-primary text-md font-semibold text-primary-foreground">
<span>{brandInitial}</span>
Expand Down
34 changes: 21 additions & 13 deletions components/dashboard-auth-guard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import { useAuth } from "@/contexts/auth-context"
import { useApiReady } from "@/contexts/api-context"
import { useS3Ready } from "@/contexts/s3-context"
import { usePermissions } from "@/hooks/use-permissions"
import { canAccessDashboardRoute } from "@/lib/dashboard-route-meta"

export function DashboardAuthGuard({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const { isAuthenticated } = useAuth()
const { isReady: apiReady } = useApiReady()
const { isReady: s3Ready } = useS3Ready()
const { isAdmin, userPolicy, isLoading, hasFetchedPolicy, fetchUserPolicy, canAccessPath } = usePermissions()
const { isAdmin, isLoading, hasFetchedPolicy, hasResolvedAdmin, canAccessPath } = usePermissions()

const isReady = apiReady && s3Ready

Expand All @@ -24,29 +25,36 @@ export function DashboardAuthGuard({ children }: { children: React.ReactNode })
}, [isAuthenticated, isReady, router])

useEffect(() => {
if (!isReady || !isAuthenticated || isAdmin) return
if (!userPolicy && !isLoading) {
void fetchUserPolicy()
}
}, [isReady, isAuthenticated, isAdmin, userPolicy, isLoading, fetchUserPolicy])

useEffect(() => {
if (!isReady || !isAuthenticated || isAdmin) return
if (isLoading || !hasFetchedPolicy) return
if (!canAccessPath(pathname)) {
if (!isReady || !isAuthenticated || !hasResolvedAdmin) return
if (!isAdmin && (isLoading || !hasFetchedPolicy)) return
if (!canAccessDashboardRoute(pathname, { isAdmin, canAccessPath })) {
router.replace("/403/")
}
}, [isReady, isAuthenticated, isAdmin, isLoading, userPolicy, hasFetchedPolicy, canAccessPath, pathname, router])
}, [
isReady,
isAuthenticated,
hasResolvedAdmin,
isAdmin,
isLoading,
hasFetchedPolicy,
canAccessPath,
pathname,
router,
])

if (!isReady || !isAuthenticated) {
return null
}

if (!hasResolvedAdmin) {
return null
}

if (!isAdmin && (isLoading || !hasFetchedPolicy)) {
return null
}

if (!isAdmin && !canAccessPath(pathname)) {
if (!canAccessDashboardRoute(pathname, { isAdmin, canAccessPath })) {
return null
}

Expand Down
12 changes: 1 addition & 11 deletions components/user/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { useAuth } from "@/contexts/auth-context"
import { usePermissions } from "@/hooks/use-permissions"
import { useUsers } from "@/hooks/use-users"
import { ChangePassword } from "./change-password"
import { useSidebar } from "@/components/ui/sidebar"
import { getThemeManifest } from "@/lib/theme/manifest"
Expand Down Expand Up @@ -41,9 +40,8 @@ export function UserDropdown() {
const { t } = useTranslation()
const router = useRouter()
const { resolvedTheme } = useTheme()
const { logout, isAdmin, setIsAdmin } = useAuth()
const { logout, isAdmin } = useAuth()
const { userInfo } = usePermissions()
const { isAdminUser } = useUsers()
const { state } = useSidebar()
const isCollapsed = state === "collapsed"
const theme = getThemeManifest()
Expand All @@ -57,14 +55,6 @@ export function UserDropdown() {
setAvatar(resolveAvatarPath(preferredAvatarPath))
}, [preferredAvatarPath])

useEffect(() => {
isAdminUser().then((adminInfo) => {
if (adminInfo) {
setIsAdmin(adminInfo.is_admin ?? false)
}
})
}, [isAdminUser, setIsAdmin])

const handleChangePassword = () => {
setChangePasswordVisible(true)
}
Expand Down
Loading
Loading