Files
app/frontend/src/hooks/useAuth.ts
T
Melvin Strobl 6d2e25c2ca Let agents drive the flow API over MCP
The engine now speaks MCP at /mcp, with a built-in OAuth 2.1 authorization
server in front of it: an agent registers itself, sends a human to the browser
to approve it, and exchanges the resulting code for a token. PKCE is required,
codes are single-use and stored only as hashes, the browser is redirected to
the URI that was registered rather than the one asked for, and refresh tokens
rotate so that replaying a spent one revokes the whole line.

Twenty tools cover reading, building, publishing and running flows, and each
one calls the same REST endpoint the dashboard calls, in-process, carrying the
caller's own token. That keeps one description of what a flow is and what may
be done to it — validation, the draft/publish split, the version check — and
means an agent can do nothing a person could not do in the browser.

Agent tokens are RS256 with a keypair of their own rather than the secret that
signs browser sessions, so deleting the key withdraws every agent without
logging anyone out, and deps.decode_token grew the branch that trusting a
second issuer will need when the hosted login arrives.
2026-08-16 00:22:41 +02:00

84 lines
2.2 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { useNavigate } from "@tanstack/react-router"
import {
type Body_login_login_access_token as AccessToken,
LoginService,
type UserPublic,
type UserRegister,
UsersService,
} from "@/client"
import { handleError } from "@/utils"
import useCustomToast from "./useCustomToast"
const isLoggedIn = () => {
return localStorage.getItem("access_token") !== null
}
/**
* Where to go after signing in.
*
* Only a path on this origin: an open redirect here would let a link take
* someone through a real login and land them somewhere else entirely.
*/
export function safeRedirect(target: string | undefined): string {
if (!target || !target.startsWith("/") || target.startsWith("//")) return "/"
return target
}
const useAuth = () => {
const navigate = useNavigate()
const queryClient = useQueryClient()
const { showErrorToast } = useCustomToast()
const { data: user } = useQuery<UserPublic | null, Error>({
queryKey: ["currentUser"],
queryFn: UsersService.readUserMe,
enabled: isLoggedIn(),
})
const signUpMutation = useMutation({
mutationFn: (data: UserRegister) =>
UsersService.registerUser({ requestBody: data }),
onSuccess: () => {
navigate({ to: "/login" })
},
onError: handleError.bind(showErrorToast),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["users"] })
},
})
const login = async (data: AccessToken) => {
const response = await LoginService.loginAccessToken({
formData: data,
})
localStorage.setItem("access_token", response.access_token)
}
const loginMutation = useMutation({
mutationFn: login,
onSuccess: () => {
// The consent page sends people here with where to come back to.
const target = new URLSearchParams(window.location.search).get("redirect")
navigate({ to: safeRedirect(target ?? undefined) })
},
onError: handleError.bind(showErrorToast),
})
const logout = () => {
localStorage.removeItem("access_token")
navigate({ to: "/login" })
}
return {
signUpMutation,
loginMutation,
logout,
user,
}
}
export { isLoggedIn }
export default useAuth