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 <cursoragent@cursor.com>
99 lines
2.5 KiB
TypeScript
99 lines
2.5 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState } from "react"
|
|
import { useRouter } from "next/navigation"
|
|
import {
|
|
clearPortalHandoffFlag,
|
|
clearToken,
|
|
ensureAuthConfig,
|
|
firstAllowedPath,
|
|
getClaims,
|
|
getToken,
|
|
parseHashToken,
|
|
redirectToPortalLogin,
|
|
redirectToPortalLoginInteractive,
|
|
setToken,
|
|
} from "@/lib/auth"
|
|
|
|
export default function AuthCallbackPage() {
|
|
const router = useRouter()
|
|
const [message, setMessage] = useState("Перенаправление на Auth Portal…")
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
|
|
void (async () => {
|
|
await ensureAuthConfig()
|
|
if (cancelled) return
|
|
|
|
const params = new URLSearchParams(window.location.search)
|
|
const error = params.get("error")
|
|
if (error === "sso_loop" || error === "jwt_rejected") {
|
|
redirectToPortalLoginInteractive()
|
|
return
|
|
}
|
|
|
|
const { accessToken } = parseHashToken(window.location.hash)
|
|
if (accessToken) {
|
|
setToken(accessToken)
|
|
clearPortalHandoffFlag()
|
|
const claims = getClaims()
|
|
if (!claims) {
|
|
clearToken()
|
|
redirectToPortalLoginInteractive()
|
|
return
|
|
}
|
|
if (!claims.apps.includes("mm")) {
|
|
setMessage("Нет доступа к приложению")
|
|
router.replace("/access-denied")
|
|
return
|
|
}
|
|
|
|
try {
|
|
const res = await fetch("/api/auth/config", {
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
})
|
|
if (res.status === 401) {
|
|
clearToken()
|
|
redirectToPortalLoginInteractive()
|
|
return
|
|
}
|
|
} catch {
|
|
/* ignore network — proceed */
|
|
}
|
|
|
|
const next = firstAllowedPath()
|
|
if (next === "/access-denied") {
|
|
router.replace("/access-denied")
|
|
return
|
|
}
|
|
router.replace(next)
|
|
return
|
|
}
|
|
|
|
if (getToken() && getClaims()) {
|
|
clearPortalHandoffFlag()
|
|
if (!getClaims()!.apps.includes("mm")) {
|
|
router.replace("/access-denied")
|
|
return
|
|
}
|
|
router.replace(firstAllowedPath())
|
|
return
|
|
}
|
|
|
|
const ok = redirectToPortalLogin(`${window.location.origin}/auth/callback`)
|
|
if (!ok) redirectToPortalLoginInteractive()
|
|
})()
|
|
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [router])
|
|
|
|
return (
|
|
<div className="text-muted-foreground flex min-h-svh items-center justify-center p-6 text-sm">
|
|
{message}
|
|
</div>
|
|
)
|
|
}
|