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.
84 lines
2.2 KiB
TypeScript
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
|