feat(auth): интегрировать SSO auth-portal
Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m52s
Docker images / frontend-image (push) Successful in 2m23s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 38s
Docker images / publish-release (push) Successful in 8s
Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m52s
Docker images / frontend-image (push) Successful in 2m23s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 38s
Docker images / publish-release (push) Successful in 8s
JWT на backend, handoff/callback на UI, RBAC mm:*, AUTH_* в compose. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState, type ReactNode } from "react"
|
||||
import {
|
||||
ensureAuthConfig,
|
||||
getClaims,
|
||||
getToken,
|
||||
isAuthEnabled,
|
||||
redirectToPortalLogin,
|
||||
redirectToPortalLoginInteractive,
|
||||
} from "@/lib/auth"
|
||||
|
||||
export function AuthGuard({ children }: { children: ReactNode }) {
|
||||
const [ready, setReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
await ensureAuthConfig()
|
||||
if (cancelled) return
|
||||
if (!isAuthEnabled()) {
|
||||
setReady(true)
|
||||
return
|
||||
}
|
||||
const claims = getClaims()
|
||||
if (!getToken() || !claims) {
|
||||
const ok = redirectToPortalLogin()
|
||||
if (!ok) redirectToPortalLoginInteractive()
|
||||
return
|
||||
}
|
||||
if (!claims.apps.includes("mm")) {
|
||||
window.location.assign("/access-denied")
|
||||
return
|
||||
}
|
||||
setReady(true)
|
||||
})()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!ready) {
|
||||
return (
|
||||
<div className="text-muted-foreground flex min-h-svh items-center justify-center p-6 text-sm">
|
||||
Проверка сессии…
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
+42
-3
@@ -5,6 +5,7 @@ import Link from "next/link"
|
||||
import { useTheme } from "@/components/theme-provider"
|
||||
import {
|
||||
ChevronsUpDownIcon,
|
||||
LogOutIcon,
|
||||
MonitorIcon,
|
||||
MoonIcon,
|
||||
PaletteIcon,
|
||||
@@ -13,6 +14,12 @@ import {
|
||||
} from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
ensureAuthConfig,
|
||||
getClaims,
|
||||
isAuthEnabled,
|
||||
redirectToPortalLogout,
|
||||
} from "@/lib/auth"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
@@ -96,11 +103,37 @@ function ThemeSegmentedToggle() {
|
||||
)
|
||||
}
|
||||
|
||||
function initials(name: string, email: string): string {
|
||||
const base = (name || email || "?").trim()
|
||||
const parts = base.split(/\s+/).filter(Boolean)
|
||||
if (parts.length >= 2) {
|
||||
return (parts[0]![0]! + parts[1]![0]!).toUpperCase()
|
||||
}
|
||||
return base.slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
export function NavUser() {
|
||||
const { isMobile } = useSidebar()
|
||||
const name = "Оператор"
|
||||
const email = "локальный доступ"
|
||||
const fallback = "ОП"
|
||||
const [name, setName] = useState("Оператор")
|
||||
const [email, setEmail] = useState("локальный доступ")
|
||||
const [showLogout, setShowLogout] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
void ensureAuthConfig().then(() => {
|
||||
const claims = getClaims()
|
||||
if (claims) {
|
||||
setName(claims.name || claims.email || "Пользователь")
|
||||
setEmail(claims.email || "")
|
||||
setShowLogout(isAuthEnabled())
|
||||
} else if (isAuthEnabled()) {
|
||||
setName("Сессия")
|
||||
setEmail("требуется вход")
|
||||
setShowLogout(true)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const fallback = initials(name, email)
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
@@ -155,6 +188,12 @@ export function NavUser() {
|
||||
<ThemeSegmentedToggle />
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
{showLogout ? (
|
||||
<DropdownMenuItem onClick={() => redirectToPortalLogout()}>
|
||||
<LogOutIcon aria-hidden />
|
||||
Выйти
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
Reference in New Issue
Block a user