mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
feat(web): switch /device page to /openapi/v1 paths (Phase G.21)
Approve/deny + lookup + SSO endpoints now live under /openapi/v1/oauth/device/*. Approve/deny use direct fetch with console session cookie + CSRF instead of the /console/api-prefixed post() helper.
This commit is contained in:
parent
a07b32274a
commit
eb5ef3dba5
@ -17,7 +17,7 @@ type Props = {
|
||||
/**
|
||||
* AuthorizeAccount is the account-branch authorize screen. Called with a
|
||||
* live console session already established (user bounced through /signin).
|
||||
* Posts to /console/api/oauth/device/{approve,deny}; these endpoints mint
|
||||
* Posts to /openapi/v1/oauth/device/{approve,deny}; these endpoints mint
|
||||
* the dfoa_ token server-side.
|
||||
*/
|
||||
const AuthorizeAccount: FC<Props> = ({
|
||||
|
||||
@ -13,9 +13,10 @@ type Props = {
|
||||
|
||||
/**
|
||||
* AuthorizeSSO is the external-SSO branch authorize screen. On mount it
|
||||
* fetches /v1/oauth/device/approval-context to learn subject_email, issuer,
|
||||
* user_code, and csrf_token from the device_approval_grant cookie. On
|
||||
* Approve click, posts /v1/oauth/device/approve-external with the CSRF header.
|
||||
* fetches /openapi/v1/oauth/device/approval-context to learn subject_email,
|
||||
* issuer, user_code, and csrf_token from the device_approval_grant cookie.
|
||||
* On Approve click, posts /openapi/v1/oauth/device/approve-external with
|
||||
* the CSRF header.
|
||||
*
|
||||
* The user_code in state is bound to the cookie by server; we do not accept
|
||||
* one from the URL because the SSO branch deliberately detaches from the
|
||||
|
||||
@ -13,7 +13,7 @@ type Props = {
|
||||
* Chooser renders the two-button device-auth login selector. Account button
|
||||
* seeds postLoginRedirect + navigates to /signin so every existing account
|
||||
* login method (password / email-code / social OAuth / account-SSO) flows
|
||||
* through its usual plumbing. SSO button hits /v1/oauth/device/sso-initiate
|
||||
* through its usual plumbing. SSO button hits /openapi/v1/oauth/device/sso-initiate
|
||||
* directly — the SSO branch skips /signin entirely.
|
||||
*
|
||||
* v1.0 scope: only account-SSO honours postLoginRedirect (via sso-auth's
|
||||
@ -31,10 +31,10 @@ const Chooser: FC<Props> = ({ userCode, ssoAvailable }) => {
|
||||
}
|
||||
|
||||
const onSSO = () => {
|
||||
// Full-page navigation, not router.push — /v1/oauth/device/sso-initiate
|
||||
// Full-page navigation, not router.push — /openapi/v1/oauth/device/sso-initiate
|
||||
// issues a 302 to the IdP. Next's client router can't follow cross-
|
||||
// origin redirects; a plain window.location assignment handles it.
|
||||
window.location.href = `/v1/oauth/device/sso-initiate?user_code=${encodeURIComponent(userCode)}`
|
||||
window.location.href = `/openapi/v1/oauth/device/sso-initiate?user_code=${encodeURIComponent(userCode)}`
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -29,7 +29,7 @@ export function normaliseUserCodeInput(raw: string): string {
|
||||
|
||||
/**
|
||||
* isValidUserCode tests whether the normalised form is a complete XXXX-XXXX
|
||||
* token suitable for submission to /console/api/oauth/device/lookup.
|
||||
* token suitable for submission to /openapi/v1/oauth/device/lookup.
|
||||
*/
|
||||
export function isValidUserCode(normalised: string): boolean {
|
||||
return /^[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(normalised)
|
||||
|
||||
@ -1,18 +1,22 @@
|
||||
// Web-side calls into the Dify device-flow endpoints:
|
||||
// Web-side calls into the Dify device-flow endpoints. All routes now sit
|
||||
// under /openapi/v1/oauth/device/* (Phase G of the openapi migration). The
|
||||
// approve/deny endpoints still require the console session cookie + CSRF
|
||||
// token; lookup is unauthenticated; the SSO branch uses cookie + per-flow
|
||||
// CSRF baked into the approval-context response.
|
||||
//
|
||||
// /v1/oauth/device/lookup (public — GET, no auth, IP-rate-limited)
|
||||
// /v1/oauth/device/approval-context (cookie-authed — GET)
|
||||
// /v1/oauth/device/approve-external (cookie-authed + CSRF — POST)
|
||||
// /console/api/oauth/device/approve (session-authed — POST)
|
||||
// /console/api/oauth/device/deny (session-authed — POST)
|
||||
// /openapi/v1/oauth/device/lookup (public — GET)
|
||||
// /openapi/v1/oauth/device/approve (cookie + CSRF — POST)
|
||||
// /openapi/v1/oauth/device/deny (cookie + CSRF — POST)
|
||||
// /openapi/v1/oauth/device/approval-context (cookie — GET)
|
||||
// /openapi/v1/oauth/device/approve-external (cookie + per-flow CSRF — POST)
|
||||
//
|
||||
// Approve/deny use the standard service/base helpers so they get console-
|
||||
// session cookies automatically. Lookup + SSO-branch endpoints sit under
|
||||
// /v1 so they ride the existing service-API gateway route.
|
||||
// /openapi/v1/* is its own URL prefix, so we bypass service/base's
|
||||
// API_PREFIX (which targets /console/api) and call fetch directly.
|
||||
|
||||
import { post } from './base'
|
||||
import Cookies from 'js-cookie'
|
||||
import { CSRF_COOKIE_NAME, CSRF_HEADER_NAME } from '@/config'
|
||||
|
||||
const DEVICE_BASE = '/v1/oauth/device'
|
||||
const DEVICE_BASE = '/openapi/v1/oauth/device'
|
||||
|
||||
// Typed error thrown by every wrapper here. The page/component layer
|
||||
// switches on `code` to choose user-facing copy / view; never render
|
||||
@ -49,6 +53,10 @@ function statusFallbackCode(status: number): string {
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
function consoleCsrfHeader(): Record<string, string> {
|
||||
return { [CSRF_HEADER_NAME]: Cookies.get(CSRF_COOKIE_NAME()) || '' }
|
||||
}
|
||||
|
||||
// ----- Account branch --------------------------------------------------------
|
||||
|
||||
export type DeviceLookupReply = {
|
||||
@ -65,13 +73,35 @@ export async function deviceLookup(user_code: string): Promise<DeviceLookupReply
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export const deviceApproveAccount = (user_code: string) =>
|
||||
post<{ status: 'approved' }>('/oauth/device/approve', { body: { user_code } })
|
||||
export async function deviceApproveAccount(user_code: string): Promise<{ status: 'approved' }> {
|
||||
const res = await fetch(`${DEVICE_BASE}/approve`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...consoleCsrfHeader(),
|
||||
},
|
||||
body: JSON.stringify({ user_code }),
|
||||
})
|
||||
if (!res.ok) await failFromResponse(res)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export const deviceDenyAccount = (user_code: string) =>
|
||||
post<{ status: 'denied' }>('/oauth/device/deny', { body: { user_code } })
|
||||
export async function deviceDenyAccount(user_code: string): Promise<{ status: 'denied' }> {
|
||||
const res = await fetch(`${DEVICE_BASE}/deny`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...consoleCsrfHeader(),
|
||||
},
|
||||
body: JSON.stringify({ user_code }),
|
||||
})
|
||||
if (!res.ok) await failFromResponse(res)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// ----- SSO branch (cookie-authed via /v1/oauth/device/*) --------------------
|
||||
// ----- SSO branch (cookie-authed via /openapi/v1/oauth/device/*) -----------
|
||||
|
||||
export type ApprovalContext = {
|
||||
subject_email: string
|
||||
|
||||
Loading…
Reference in New Issue
Block a user