补充行业凭证授权换 token
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
exchangeAdminKuaishouIndustryAuthorizationCode,
|
||||||
getAdminKuaishouIndustrySourceConfig,
|
getAdminKuaishouIndustrySourceConfig,
|
||||||
refreshAdminKuaishouIndustryAccessToken,
|
refreshAdminKuaishouIndustryAccessToken,
|
||||||
updateAdminKuaishouIndustrySourceConfig,
|
updateAdminKuaishouIndustrySourceConfig,
|
||||||
} from '../../../services/admin/platform-config/kuaishou-industry-service.js'
|
} from '../../../services/admin/platform-config/kuaishou-industry-service.js'
|
||||||
import type {
|
import type {
|
||||||
|
AdminKuaishouIndustryAuthorizationCodeRouteBody,
|
||||||
AdminKuaishouIndustrySourceConfigRouteBody,
|
AdminKuaishouIndustrySourceConfigRouteBody,
|
||||||
} from '../../../types/admin/route-inputs.js'
|
} from '../../../types/admin/route-inputs.js'
|
||||||
import { createJsonHandler } from '../session.js'
|
import { createJsonHandler } from '../session.js'
|
||||||
@@ -75,4 +77,33 @@ router.post(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/kuaishou-industry-source/exchange-code',
|
||||||
|
createJsonHandler(
|
||||||
|
(req) =>
|
||||||
|
exchangeAdminKuaishouIndustryAuthorizationCode(
|
||||||
|
req.body as AdminKuaishouIndustryAuthorizationCodeRouteBody,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
successMessage: '快手行业电子凭证授权 token 已保存',
|
||||||
|
errorMessage: '换取快手行业电子凭证授权 token 失败',
|
||||||
|
scope: '[admin/platform-config/kuaishou-industry-source/exchange-code]',
|
||||||
|
audit: (_req, data) => {
|
||||||
|
const result = data as JsonRecord
|
||||||
|
return {
|
||||||
|
action: 'platform_kuaishou_industry_authorization_code_exchanged',
|
||||||
|
targetType: 'platform_config',
|
||||||
|
targetId: 'kuaishou_industry_source',
|
||||||
|
data: {
|
||||||
|
refreshed: Boolean(result.refreshed),
|
||||||
|
accessTokenExpiresAt: String(result.source?.accessTokenExpiresAt || '').trim(),
|
||||||
|
refreshTokenExpiresAt: String(result.source?.refreshTokenExpiresAt || '').trim(),
|
||||||
|
accessTokenStatus: String(result.source?.accessTokenStatus || '').trim(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { maskSecret } from '../../../utils/masking.js'
|
import { maskSecret } from '../../../utils/masking.js'
|
||||||
|
import { createHttpError } from '../../../utils/http.js'
|
||||||
import {
|
import {
|
||||||
getKuaishouIndustrySourceConfig,
|
getKuaishouIndustrySourceConfig,
|
||||||
getKuaishouIndustrySourceFilePath,
|
getKuaishouIndustrySourceFilePath,
|
||||||
saveKuaishouIndustrySourceConfig,
|
saveKuaishouIndustrySourceConfig,
|
||||||
type KuaishouIndustrySourceConfig,
|
type KuaishouIndustrySourceConfig,
|
||||||
} from '../../platforms/kuaishou-industry/source-config-service.js'
|
} from '../../platforms/kuaishou-industry/source-config-service.js'
|
||||||
import { refreshKuaishouIndustryAccessToken } from '../../platforms/kuaishou-industry/token-service.js'
|
import {
|
||||||
|
exchangeKuaishouIndustryAuthorizationCode,
|
||||||
|
refreshKuaishouIndustryAccessToken,
|
||||||
|
} from '../../platforms/kuaishou-industry/token-service.js'
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
type JsonObject = Record<string, any>
|
||||||
|
|
||||||
@@ -36,7 +40,12 @@ export function updateAdminKuaishouIndustrySourceConfig(payload: JsonObject = {}
|
|||||||
: current.sendCallbackEnabled === true,
|
: current.sendCallbackEnabled === true,
|
||||||
baseUrl: readConfigString(payload, 'baseUrl', current.baseUrl),
|
baseUrl: readConfigString(payload, 'baseUrl', current.baseUrl),
|
||||||
authBaseUrl: readConfigString(payload, 'authBaseUrl', current.authBaseUrl),
|
authBaseUrl: readConfigString(payload, 'authBaseUrl', current.authBaseUrl),
|
||||||
|
redirectUri: readConfigString(payload, 'redirectUri', current.redirectUri, { allowBlank: true }),
|
||||||
|
scopes: normalizeScopeText(readConfigString(payload, 'scopes', current.scopes, { allowBlank: true })),
|
||||||
|
authState: readConfigString(payload, 'authState', current.authState, { allowBlank: true }),
|
||||||
appKey: readConfigString(payload, 'appKey', current.appKey, { allowBlank: true }),
|
appKey: readConfigString(payload, 'appKey', current.appKey, { allowBlank: true }),
|
||||||
|
openId: readConfigString(payload, 'openId', current.openId, { allowBlank: true }),
|
||||||
|
grantedScopes: normalizeScopeText(readConfigString(payload, 'grantedScopes', current.grantedScopes, { allowBlank: true })),
|
||||||
sellerId: readConfigString(payload, 'sellerId', current.sellerId, { allowBlank: true }),
|
sellerId: readConfigString(payload, 'sellerId', current.sellerId, { allowBlank: true }),
|
||||||
provider: readConfigString(payload, 'provider', current.provider),
|
provider: readConfigString(payload, 'provider', current.provider),
|
||||||
platform: readConfigString(payload, 'platform', current.platform),
|
platform: readConfigString(payload, 'platform', current.platform),
|
||||||
@@ -64,6 +73,24 @@ export async function refreshAdminKuaishouIndustryAccessToken() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function exchangeAdminKuaishouIndustryAuthorizationCode(payload: JsonObject = {}) {
|
||||||
|
const code = String(payload.code || '').trim()
|
||||||
|
if (!code) {
|
||||||
|
throw createHttpError('授权 code 未填写', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'kuaishou_industry_missing_authorization_code',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await exchangeKuaishouIndustryAuthorizationCode(code)
|
||||||
|
|
||||||
|
return {
|
||||||
|
filePath: getKuaishouIndustrySourceFilePath(),
|
||||||
|
refreshed: result.refreshed,
|
||||||
|
source: mapAdminKuaishouIndustrySourceConfig(result.config),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mapAdminKuaishouIndustrySourceConfig(config: KuaishouIndustrySourceConfig) {
|
function mapAdminKuaishouIndustrySourceConfig(config: KuaishouIndustrySourceConfig) {
|
||||||
const accessTokenStatus = resolveAccessTokenStatus(config)
|
const accessTokenStatus = resolveAccessTokenStatus(config)
|
||||||
|
|
||||||
@@ -72,6 +99,10 @@ function mapAdminKuaishouIndustrySourceConfig(config: KuaishouIndustrySourceConf
|
|||||||
sendCallbackEnabled: config.sendCallbackEnabled === true,
|
sendCallbackEnabled: config.sendCallbackEnabled === true,
|
||||||
baseUrl: config.baseUrl,
|
baseUrl: config.baseUrl,
|
||||||
authBaseUrl: config.authBaseUrl,
|
authBaseUrl: config.authBaseUrl,
|
||||||
|
redirectUri: config.redirectUri,
|
||||||
|
scopes: config.scopes,
|
||||||
|
authState: config.authState,
|
||||||
|
authorizationUrl: buildKuaishouIndustryAuthorizationUrl(config),
|
||||||
appKey: config.appKey,
|
appKey: config.appKey,
|
||||||
appSecret: '',
|
appSecret: '',
|
||||||
appSecretMasked: maskSecret(config.appSecret),
|
appSecretMasked: maskSecret(config.appSecret),
|
||||||
@@ -92,6 +123,8 @@ function mapAdminKuaishouIndustrySourceConfig(config: KuaishouIndustrySourceConf
|
|||||||
refreshTokenExpiresAt: config.refreshTokenExpiresAt,
|
refreshTokenExpiresAt: config.refreshTokenExpiresAt,
|
||||||
accessTokenStatus: accessTokenStatus.status,
|
accessTokenStatus: accessTokenStatus.status,
|
||||||
accessTokenExpiresInSeconds: accessTokenStatus.expiresInSeconds,
|
accessTokenExpiresInSeconds: accessTokenStatus.expiresInSeconds,
|
||||||
|
openId: config.openId,
|
||||||
|
grantedScopes: config.grantedScopes,
|
||||||
sellerId: config.sellerId,
|
sellerId: config.sellerId,
|
||||||
provider: config.provider,
|
provider: config.provider,
|
||||||
platform: config.platform,
|
platform: config.platform,
|
||||||
@@ -103,6 +136,35 @@ function mapAdminKuaishouIndustrySourceConfig(config: KuaishouIndustrySourceConf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildKuaishouIndustryAuthorizationUrl(config: KuaishouIndustrySourceConfig): string {
|
||||||
|
if (!config.authBaseUrl || !config.appKey || !config.redirectUri || !config.scopes) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(`${String(config.authBaseUrl).replace(/\/+$/, '')}/oauth/authorize`)
|
||||||
|
url.searchParams.set('app_id', config.appKey)
|
||||||
|
url.searchParams.set('redirect_uri', config.redirectUri)
|
||||||
|
url.searchParams.set('scope', normalizeScopeText(config.scopes))
|
||||||
|
url.searchParams.set('response_type', 'code')
|
||||||
|
if (config.authState) {
|
||||||
|
url.searchParams.set('state', config.authState)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.toString()
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeScopeText(value: unknown): string {
|
||||||
|
return String(value || '')
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
function resolveSecretPatch(
|
function resolveSecretPatch(
|
||||||
payload: JsonObject,
|
payload: JsonObject,
|
||||||
current: KuaishouIndustrySourceConfig,
|
current: KuaishouIndustrySourceConfig,
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export function getKuaishouIndustryConfig(overrides: Partial<KuaishouIndustrySou
|
|||||||
enabled: config.enabled !== false,
|
enabled: config.enabled !== false,
|
||||||
baseUrl: String(config.baseUrl || '').trim(),
|
baseUrl: String(config.baseUrl || '').trim(),
|
||||||
authBaseUrl: String(config.authBaseUrl || '').trim(),
|
authBaseUrl: String(config.authBaseUrl || '').trim(),
|
||||||
|
redirectUri: String(config.redirectUri || '').trim(),
|
||||||
|
scopes: String(config.scopes || '').trim(),
|
||||||
|
authState: String(config.authState || '').trim(),
|
||||||
appKey: String(config.appKey || '').trim(),
|
appKey: String(config.appKey || '').trim(),
|
||||||
appSecret: String(config.appSecret || '').trim(),
|
appSecret: String(config.appSecret || '').trim(),
|
||||||
signSecret: String(config.signSecret || '').trim(),
|
signSecret: String(config.signSecret || '').trim(),
|
||||||
@@ -25,6 +28,8 @@ export function getKuaishouIndustryConfig(overrides: Partial<KuaishouIndustrySou
|
|||||||
refreshToken: String(config.refreshToken || '').trim(),
|
refreshToken: String(config.refreshToken || '').trim(),
|
||||||
accessTokenExpiresAt: String(config.accessTokenExpiresAt || '').trim(),
|
accessTokenExpiresAt: String(config.accessTokenExpiresAt || '').trim(),
|
||||||
refreshTokenExpiresAt: String(config.refreshTokenExpiresAt || '').trim(),
|
refreshTokenExpiresAt: String(config.refreshTokenExpiresAt || '').trim(),
|
||||||
|
openId: String(config.openId || '').trim(),
|
||||||
|
grantedScopes: String(config.grantedScopes || '').trim(),
|
||||||
sellerId: String(config.sellerId || '').trim(),
|
sellerId: String(config.sellerId || '').trim(),
|
||||||
lastRefreshedAt: String(config.lastRefreshedAt || '').trim(),
|
lastRefreshedAt: String(config.lastRefreshedAt || '').trim(),
|
||||||
lastRefreshError: String(config.lastRefreshError || '').trim(),
|
lastRefreshError: String(config.lastRefreshError || '').trim(),
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ export type KuaishouIndustrySourceConfig = {
|
|||||||
sendCallbackEnabled: boolean
|
sendCallbackEnabled: boolean
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
authBaseUrl: string
|
authBaseUrl: string
|
||||||
|
redirectUri: string
|
||||||
|
scopes: string
|
||||||
|
authState: string
|
||||||
appKey: string
|
appKey: string
|
||||||
appSecret: string
|
appSecret: string
|
||||||
signSecret: string
|
signSecret: string
|
||||||
@@ -22,6 +25,8 @@ export type KuaishouIndustrySourceConfig = {
|
|||||||
refreshToken: string
|
refreshToken: string
|
||||||
accessTokenExpiresAt: string
|
accessTokenExpiresAt: string
|
||||||
refreshTokenExpiresAt: string
|
refreshTokenExpiresAt: string
|
||||||
|
openId: string
|
||||||
|
grantedScopes: string
|
||||||
sellerId: string
|
sellerId: string
|
||||||
provider: string
|
provider: string
|
||||||
platform: string
|
platform: string
|
||||||
@@ -72,6 +77,9 @@ function normalizeKuaishouIndustrySourceConfig(rawValue: unknown): KuaishouIndus
|
|||||||
: fallback.sendCallbackEnabled,
|
: fallback.sendCallbackEnabled,
|
||||||
baseUrl: normalizeUrlLike(source.baseUrl, fallback.baseUrl),
|
baseUrl: normalizeUrlLike(source.baseUrl, fallback.baseUrl),
|
||||||
authBaseUrl: normalizeUrlLike(source.authBaseUrl, fallback.authBaseUrl),
|
authBaseUrl: normalizeUrlLike(source.authBaseUrl, fallback.authBaseUrl),
|
||||||
|
redirectUri: normalizeString(source.redirectUri, fallback.redirectUri),
|
||||||
|
scopes: normalizeString(source.scopes, fallback.scopes),
|
||||||
|
authState: normalizeString(source.authState, fallback.authState),
|
||||||
appKey: normalizeString(source.appKey, fallback.appKey),
|
appKey: normalizeString(source.appKey, fallback.appKey),
|
||||||
appSecret: normalizeString(source.appSecret, fallback.appSecret),
|
appSecret: normalizeString(source.appSecret, fallback.appSecret),
|
||||||
signSecret: normalizeString(source.signSecret, fallback.signSecret),
|
signSecret: normalizeString(source.signSecret, fallback.signSecret),
|
||||||
@@ -80,6 +88,8 @@ function normalizeKuaishouIndustrySourceConfig(rawValue: unknown): KuaishouIndus
|
|||||||
refreshToken: normalizeString(source.refreshToken, fallback.refreshToken),
|
refreshToken: normalizeString(source.refreshToken, fallback.refreshToken),
|
||||||
accessTokenExpiresAt: normalizeNullableIso(source.accessTokenExpiresAt),
|
accessTokenExpiresAt: normalizeNullableIso(source.accessTokenExpiresAt),
|
||||||
refreshTokenExpiresAt: normalizeNullableIso(source.refreshTokenExpiresAt),
|
refreshTokenExpiresAt: normalizeNullableIso(source.refreshTokenExpiresAt),
|
||||||
|
openId: normalizeString(source.openId, fallback.openId),
|
||||||
|
grantedScopes: normalizeString(source.grantedScopes, fallback.grantedScopes),
|
||||||
sellerId: normalizeString(source.sellerId, fallback.sellerId),
|
sellerId: normalizeString(source.sellerId, fallback.sellerId),
|
||||||
provider: normalizeString(source.provider, fallback.provider),
|
provider: normalizeString(source.provider, fallback.provider),
|
||||||
platform: normalizeString(source.platform, fallback.platform),
|
platform: normalizeString(source.platform, fallback.platform),
|
||||||
@@ -99,6 +109,9 @@ function createDefaultKuaishouIndustrySourceConfig(): KuaishouIndustrySourceConf
|
|||||||
sendCallbackEnabled: Boolean(runtime.sendCallbackEnabled),
|
sendCallbackEnabled: Boolean(runtime.sendCallbackEnabled),
|
||||||
baseUrl: DEFAULT_CALLBACK_BASE_URL,
|
baseUrl: DEFAULT_CALLBACK_BASE_URL,
|
||||||
authBaseUrl: DEFAULT_AUTH_BASE_URL,
|
authBaseUrl: DEFAULT_AUTH_BASE_URL,
|
||||||
|
redirectUri: '',
|
||||||
|
scopes: '',
|
||||||
|
authState: '',
|
||||||
appKey: String(runtime.appKey || '').trim(),
|
appKey: String(runtime.appKey || '').trim(),
|
||||||
appSecret: String(runtime.appSecret || '').trim(),
|
appSecret: String(runtime.appSecret || '').trim(),
|
||||||
signSecret: String(runtime.signSecret || '').trim(),
|
signSecret: String(runtime.signSecret || '').trim(),
|
||||||
@@ -107,6 +120,8 @@ function createDefaultKuaishouIndustrySourceConfig(): KuaishouIndustrySourceConf
|
|||||||
refreshToken: '',
|
refreshToken: '',
|
||||||
accessTokenExpiresAt: '',
|
accessTokenExpiresAt: '',
|
||||||
refreshTokenExpiresAt: '',
|
refreshTokenExpiresAt: '',
|
||||||
|
openId: '',
|
||||||
|
grantedScopes: '',
|
||||||
sellerId: '',
|
sellerId: '',
|
||||||
provider: String(runtime.provider || 'kuaishou-industry').trim() || 'kuaishou-industry',
|
provider: String(runtime.provider || 'kuaishou-industry').trim() || 'kuaishou-industry',
|
||||||
platform: String(runtime.platform || 'kuaishou').trim() || 'kuaishou',
|
platform: String(runtime.platform || 'kuaishou').trim() || 'kuaishou',
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ function createConfig(
|
|||||||
sendCallbackEnabled: true,
|
sendCallbackEnabled: true,
|
||||||
baseUrl: 'https://openapi.kwaixiaodian.com',
|
baseUrl: 'https://openapi.kwaixiaodian.com',
|
||||||
authBaseUrl: 'https://open.kwaixiaodian.com',
|
authBaseUrl: 'https://open.kwaixiaodian.com',
|
||||||
|
redirectUri: '',
|
||||||
|
scopes: '',
|
||||||
|
authState: '',
|
||||||
appKey: 'app-key',
|
appKey: 'app-key',
|
||||||
appSecret: 'app-secret',
|
appSecret: 'app-secret',
|
||||||
signSecret: 'sign-secret',
|
signSecret: 'sign-secret',
|
||||||
@@ -53,6 +56,8 @@ function createConfig(
|
|||||||
refreshToken: 'refresh-token',
|
refreshToken: 'refresh-token',
|
||||||
accessTokenExpiresAt: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
|
accessTokenExpiresAt: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
|
||||||
refreshTokenExpiresAt: '',
|
refreshTokenExpiresAt: '',
|
||||||
|
openId: '',
|
||||||
|
grantedScopes: '',
|
||||||
sellerId: '',
|
sellerId: '',
|
||||||
provider: 'kuaishou-industry',
|
provider: 'kuaishou-industry',
|
||||||
platform: 'kuaishou',
|
platform: 'kuaishou',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type JsonObject = Record<string, any>
|
|||||||
|
|
||||||
const ACCESS_TOKEN_REFRESH_MARGIN_MS = 30 * 60 * 1000
|
const ACCESS_TOKEN_REFRESH_MARGIN_MS = 30 * 60 * 1000
|
||||||
const DEFAULT_ACCESS_TOKEN_TTL_MS = 47 * 60 * 60 * 1000
|
const DEFAULT_ACCESS_TOKEN_TTL_MS = 47 * 60 * 60 * 1000
|
||||||
|
const DEFAULT_REFRESH_TOKEN_TTL_MS = 179 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
export type KuaishouIndustryAccessTokenResult = {
|
export type KuaishouIndustryAccessTokenResult = {
|
||||||
accessToken: string
|
accessToken: string
|
||||||
@@ -43,40 +44,23 @@ export async function refreshKuaishouIndustryAccessToken(
|
|||||||
): Promise<KuaishouIndustryAccessTokenResult> {
|
): Promise<KuaishouIndustryAccessTokenResult> {
|
||||||
assertRefreshConfig(config)
|
assertRefreshConfig(config)
|
||||||
|
|
||||||
const authBaseUrl = String(config.authBaseUrl || '').trim().replace(/\/+$/, '')
|
const params = new URLSearchParams()
|
||||||
const url = new URL(`${authBaseUrl}/oauth2/refresh_token`)
|
params.set('app_id', config.appKey)
|
||||||
url.searchParams.set('app_id', config.appKey)
|
params.set('app_secret', config.appSecret)
|
||||||
url.searchParams.set('app_secret', config.appSecret)
|
params.set('grant_type', 'refresh_token')
|
||||||
url.searchParams.set('grant_type', 'refresh_token')
|
params.set('refresh_token', config.refreshToken)
|
||||||
url.searchParams.set('refresh_token', config.refreshToken)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const startedAt = Date.now()
|
const startedAt = Date.now()
|
||||||
const response = await fetch(url)
|
const tokenPayload = await requestKuaishouIndustryToken({
|
||||||
const text = await response.text()
|
config,
|
||||||
const json = parseJsonObject(text)
|
path: '/oauth2/refresh_token',
|
||||||
const tokenPayload = normalizeTokenRefreshResponse(json, config)
|
params,
|
||||||
|
method: 'POST',
|
||||||
if (!response.ok || !tokenPayload.accessToken) {
|
failureMessage: '快手 accessToken 刷新失败',
|
||||||
const message = resolveTokenRefreshErrorMessage(json, response.status)
|
|
||||||
patchKuaishouIndustrySourceConfig({
|
|
||||||
lastRefreshError: message,
|
|
||||||
})
|
|
||||||
throw createHttpError(message, {
|
|
||||||
statusCode: 502,
|
|
||||||
errorCode: 'kuaishou_industry_access_token_refresh_failed',
|
errorCode: 'kuaishou_industry_access_token_refresh_failed',
|
||||||
})
|
})
|
||||||
}
|
const saved = saveTokenPayload(tokenPayload, config)
|
||||||
|
|
||||||
const saved = patchKuaishouIndustrySourceConfig({
|
|
||||||
accessToken: tokenPayload.accessToken,
|
|
||||||
refreshToken: tokenPayload.refreshToken || config.refreshToken,
|
|
||||||
accessTokenExpiresAt: tokenPayload.accessTokenExpiresAt,
|
|
||||||
refreshTokenExpiresAt: tokenPayload.refreshTokenExpiresAt || config.refreshTokenExpiresAt,
|
|
||||||
sellerId: tokenPayload.sellerId || config.sellerId,
|
|
||||||
lastRefreshedAt: new Date().toISOString(),
|
|
||||||
lastRefreshError: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
logInfo('[kuaishou-industry/token]', 'accessToken 刷新成功', {
|
logInfo('[kuaishou-industry/token]', 'accessToken 刷新成功', {
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
@@ -100,6 +84,52 @@ export async function refreshKuaishouIndustryAccessToken(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function exchangeKuaishouIndustryAuthorizationCode(
|
||||||
|
code: string,
|
||||||
|
config: KuaishouIndustrySourceConfig = getKuaishouIndustrySourceConfig(),
|
||||||
|
): Promise<KuaishouIndustryAccessTokenResult> {
|
||||||
|
assertAuthorizationCodeConfig(config, code)
|
||||||
|
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('app_id', config.appKey)
|
||||||
|
params.set('app_secret', config.appSecret)
|
||||||
|
params.set('grant_type', 'code')
|
||||||
|
params.set('code', code.trim())
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startedAt = Date.now()
|
||||||
|
const tokenPayload = await requestKuaishouIndustryToken({
|
||||||
|
config,
|
||||||
|
path: '/oauth2/access_token',
|
||||||
|
params,
|
||||||
|
method: 'GET',
|
||||||
|
failureMessage: '快手授权码换取 accessToken 失败',
|
||||||
|
errorCode: 'kuaishou_industry_authorization_code_exchange_failed',
|
||||||
|
})
|
||||||
|
const saved = saveTokenPayload(tokenPayload, config)
|
||||||
|
|
||||||
|
logInfo('[kuaishou-industry/token]', '授权码换取 accessToken 成功', {
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
accessTokenExpiresAt: saved.accessTokenExpiresAt || '',
|
||||||
|
refreshTokenExpiresAt: saved.refreshTokenExpiresAt || '',
|
||||||
|
hasRefreshToken: Boolean(saved.refreshToken),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: saved.accessToken,
|
||||||
|
refreshed: true,
|
||||||
|
config: saved,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
patchKuaishouIndustrySourceConfig({
|
||||||
|
lastRefreshError: message,
|
||||||
|
})
|
||||||
|
logWarn('[kuaishou-industry/token]', '授权码换取 accessToken 失败', resolveRefreshErrorDetail(error))
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function shouldRefreshAccessToken(config: KuaishouIndustrySourceConfig): boolean {
|
export function shouldRefreshAccessToken(config: KuaishouIndustrySourceConfig): boolean {
|
||||||
if (!String(config.accessToken || '').trim()) {
|
if (!String(config.accessToken || '').trim()) {
|
||||||
return true
|
return true
|
||||||
@@ -114,10 +144,10 @@ export function shouldRefreshAccessToken(config: KuaishouIndustrySourceConfig):
|
|||||||
}
|
}
|
||||||
|
|
||||||
function assertRefreshConfig(config: KuaishouIndustrySourceConfig) {
|
function assertRefreshConfig(config: KuaishouIndustrySourceConfig) {
|
||||||
if (!config.authBaseUrl) {
|
if (!config.baseUrl) {
|
||||||
throw createHttpError('快手开放平台授权地址未配置', {
|
throw createHttpError('快手开放平台 API 地址未配置', {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
errorCode: 'kuaishou_industry_missing_auth_base_url',
|
errorCode: 'kuaishou_industry_missing_base_url',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +166,92 @@ function assertRefreshConfig(config: KuaishouIndustrySourceConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeTokenRefreshResponse(
|
function assertAuthorizationCodeConfig(config: KuaishouIndustrySourceConfig, code: string) {
|
||||||
|
if (!config.baseUrl) {
|
||||||
|
throw createHttpError('快手开放平台 API 地址未配置', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'kuaishou_industry_missing_base_url',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.appKey || !config.appSecret) {
|
||||||
|
throw createHttpError('快手 appKey/appSecret 未配置,无法换取 accessToken', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'kuaishou_industry_missing_oauth_credential',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!String(code || '').trim()) {
|
||||||
|
throw createHttpError('授权 code 未填写,无法换取 accessToken', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'kuaishou_industry_missing_authorization_code',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestKuaishouIndustryToken({
|
||||||
|
config,
|
||||||
|
path,
|
||||||
|
params,
|
||||||
|
method,
|
||||||
|
failureMessage,
|
||||||
|
errorCode,
|
||||||
|
}: {
|
||||||
|
config: KuaishouIndustrySourceConfig
|
||||||
|
path: string
|
||||||
|
params: URLSearchParams
|
||||||
|
method: 'GET' | 'POST'
|
||||||
|
failureMessage: string
|
||||||
|
errorCode: string
|
||||||
|
}) {
|
||||||
|
const baseUrl = String(config.baseUrl || '').trim().replace(/\/+$/, '')
|
||||||
|
const url = new URL(`${baseUrl}${path}`)
|
||||||
|
const response = method === 'GET'
|
||||||
|
? await fetch(appendSearchParams(url, params))
|
||||||
|
: await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: params.toString(),
|
||||||
|
})
|
||||||
|
const text = await response.text()
|
||||||
|
const json = parseJsonObject(text)
|
||||||
|
const tokenPayload = normalizeTokenResponse(json, config)
|
||||||
|
|
||||||
|
if (!response.ok || !isTokenResponseSuccess(json) || !tokenPayload.accessToken) {
|
||||||
|
const message = resolveTokenErrorMessage(json, response.status, failureMessage)
|
||||||
|
patchKuaishouIndustrySourceConfig({
|
||||||
|
lastRefreshError: message,
|
||||||
|
})
|
||||||
|
throw createHttpError(message, {
|
||||||
|
statusCode: 502,
|
||||||
|
errorCode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTokenPayload(
|
||||||
|
tokenPayload: ReturnType<typeof normalizeTokenResponse>,
|
||||||
|
config: KuaishouIndustrySourceConfig,
|
||||||
|
) {
|
||||||
|
const nextRefreshToken = tokenPayload.refreshToken || config.refreshToken
|
||||||
|
|
||||||
|
return patchKuaishouIndustrySourceConfig({
|
||||||
|
accessToken: tokenPayload.accessToken,
|
||||||
|
refreshToken: nextRefreshToken,
|
||||||
|
accessTokenExpiresAt: tokenPayload.accessTokenExpiresAt,
|
||||||
|
refreshTokenExpiresAt: tokenPayload.refreshTokenExpiresAt
|
||||||
|
|| (tokenPayload.refreshToken ? new Date(Date.now() + DEFAULT_REFRESH_TOKEN_TTL_MS).toISOString() : config.refreshTokenExpiresAt),
|
||||||
|
sellerId: tokenPayload.sellerId || config.sellerId,
|
||||||
|
openId: tokenPayload.openId || config.openId,
|
||||||
|
grantedScopes: tokenPayload.grantedScopes || config.grantedScopes,
|
||||||
|
lastRefreshedAt: new Date().toISOString(),
|
||||||
|
lastRefreshError: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTokenResponse(
|
||||||
json: JsonObject,
|
json: JsonObject,
|
||||||
currentConfig: KuaishouIndustrySourceConfig,
|
currentConfig: KuaishouIndustrySourceConfig,
|
||||||
) {
|
) {
|
||||||
@@ -152,11 +267,15 @@ function normalizeTokenRefreshResponse(
|
|||||||
'refreshTokenValue',
|
'refreshTokenValue',
|
||||||
])
|
])
|
||||||
const sellerId = pickFirstString(payload, ['seller_id', 'sellerId'])
|
const sellerId = pickFirstString(payload, ['seller_id', 'sellerId'])
|
||||||
|
const openId = pickFirstString(payload, ['open_id', 'openId'])
|
||||||
|
const grantedScopes = pickScopes(payload)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
sellerId,
|
sellerId,
|
||||||
|
openId,
|
||||||
|
grantedScopes,
|
||||||
accessTokenExpiresAt: resolveExpiresAt(payload, [
|
accessTokenExpiresAt: resolveExpiresAt(payload, [
|
||||||
'access_token_expires_at',
|
'access_token_expires_at',
|
||||||
'accessTokenExpiresAt',
|
'accessTokenExpiresAt',
|
||||||
@@ -176,10 +295,18 @@ function normalizeTokenRefreshResponse(
|
|||||||
], [
|
], [
|
||||||
'refresh_token_expires_in',
|
'refresh_token_expires_in',
|
||||||
'refreshTokenExpiresIn',
|
'refreshTokenExpiresIn',
|
||||||
], 0) || currentConfig.refreshTokenExpiresAt,
|
], 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendSearchParams(url: URL, params: URLSearchParams): URL {
|
||||||
|
for (const [key, value] of params.entries()) {
|
||||||
|
url.searchParams.set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
function resolveExpiresAt(
|
function resolveExpiresAt(
|
||||||
payload: JsonObject,
|
payload: JsonObject,
|
||||||
dateKeys: string[],
|
dateKeys: string[],
|
||||||
@@ -218,7 +345,17 @@ function parseTimestampLike(value: unknown): number {
|
|||||||
return Date.parse(String(value || '').trim())
|
return Date.parse(String(value || '').trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTokenRefreshErrorMessage(json: JsonObject, status: number): string {
|
function isTokenResponseSuccess(json: JsonObject): boolean {
|
||||||
|
const payload = isPlainObject(json.data) ? json.data : json
|
||||||
|
const result = payload.result ?? json.result
|
||||||
|
if (result === undefined || result === null || result === '') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(result) === 1 || result === true || String(result).toLowerCase() === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTokenErrorMessage(json: JsonObject, status: number, fallback: string): string {
|
||||||
const payload = isPlainObject(json.data) ? json.data : json
|
const payload = isPlainObject(json.data) ? json.data : json
|
||||||
return pickFirstString(payload, [
|
return pickFirstString(payload, [
|
||||||
'error_msg',
|
'error_msg',
|
||||||
@@ -227,7 +364,7 @@ function resolveTokenRefreshErrorMessage(json: JsonObject, status: number): stri
|
|||||||
'msg',
|
'msg',
|
||||||
'error_description',
|
'error_description',
|
||||||
'errorDescription',
|
'errorDescription',
|
||||||
]) || `快手 accessToken 刷新失败,HTTP ${status}`
|
]) || `${fallback},HTTP ${status}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseJsonObject(text: string): JsonObject {
|
function parseJsonObject(text: string): JsonObject {
|
||||||
@@ -250,6 +387,15 @@ function pickFirstString(payload: JsonObject, keys: string[]): string {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pickScopes(payload: JsonObject): string {
|
||||||
|
const value = payload.scopes ?? payload.scope
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => String(item || '').trim()).filter(Boolean).join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
function resolveRefreshErrorDetail(error: unknown): JsonObject {
|
function resolveRefreshErrorDetail(error: unknown): JsonObject {
|
||||||
const detail: JsonObject = {
|
const detail: JsonObject = {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
AdminCloudtentaclesSkuBuyInput,
|
AdminCloudtentaclesSkuBuyInput,
|
||||||
AdminCloudtentaclesSkuUseInput,
|
AdminCloudtentaclesSkuUseInput,
|
||||||
AdminCloudtentaclesSourceConfigInput,
|
AdminCloudtentaclesSourceConfigInput,
|
||||||
|
AdminKuaishouIndustryAuthorizationCodeInput,
|
||||||
AdminKuaishouIndustrySourceConfigInput,
|
AdminKuaishouIndustrySourceConfigInput,
|
||||||
AdminCloudtentaclesTestLoginInput,
|
AdminCloudtentaclesTestLoginInput,
|
||||||
AdminCloudtentaclesValidateSessionInput,
|
AdminCloudtentaclesValidateSessionInput,
|
||||||
@@ -33,6 +34,7 @@ export type AdminRouteAdminSession = AdminViewerSessionInput
|
|||||||
|
|
||||||
export type AdminKuaishouEticketSourceConfigRouteBody = AdminKuaishouEticketSourceConfigInput
|
export type AdminKuaishouEticketSourceConfigRouteBody = AdminKuaishouEticketSourceConfigInput
|
||||||
export type AdminKuaishouIndustrySourceConfigRouteBody = AdminKuaishouIndustrySourceConfigInput
|
export type AdminKuaishouIndustrySourceConfigRouteBody = AdminKuaishouIndustrySourceConfigInput
|
||||||
|
export type AdminKuaishouIndustryAuthorizationCodeRouteBody = AdminKuaishouIndustryAuthorizationCodeInput
|
||||||
export type AdminNotificationConfigRouteBody = AdminNotificationConfigInput
|
export type AdminNotificationConfigRouteBody = AdminNotificationConfigInput
|
||||||
export type AdminNotificationTestRouteBody = AdminNotificationTestInput
|
export type AdminNotificationTestRouteBody = AdminNotificationTestInput
|
||||||
export type AdminScheduledJobsConfigRouteBody = AdminScheduledJobsConfigInput
|
export type AdminScheduledJobsConfigRouteBody = AdminScheduledJobsConfigInput
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export type AdminKuaishouIndustrySourceConfigInput = {
|
|||||||
sendCallbackEnabled?: boolean
|
sendCallbackEnabled?: boolean
|
||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
authBaseUrl?: string
|
authBaseUrl?: string
|
||||||
|
redirectUri?: string
|
||||||
|
scopes?: string
|
||||||
|
authState?: string
|
||||||
appKey?: string
|
appKey?: string
|
||||||
appSecret?: string
|
appSecret?: string
|
||||||
signSecret?: string
|
signSecret?: string
|
||||||
@@ -25,6 +28,8 @@ export type AdminKuaishouIndustrySourceConfigInput = {
|
|||||||
refreshToken?: string
|
refreshToken?: string
|
||||||
accessTokenExpiresAt?: string
|
accessTokenExpiresAt?: string
|
||||||
refreshTokenExpiresAt?: string
|
refreshTokenExpiresAt?: string
|
||||||
|
openId?: string
|
||||||
|
grantedScopes?: string
|
||||||
sellerId?: string
|
sellerId?: string
|
||||||
provider?: string
|
provider?: string
|
||||||
platform?: string
|
platform?: string
|
||||||
@@ -33,6 +38,10 @@ export type AdminKuaishouIndustrySourceConfigInput = {
|
|||||||
version?: string
|
version?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AdminKuaishouIndustryAuthorizationCodeInput = {
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type AdminNotificationBarkRecipientInput = {
|
export type AdminNotificationBarkRecipientInput = {
|
||||||
id?: string
|
id?: string
|
||||||
name?: string
|
name?: string
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
|
CopyOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
|
LinkOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
@@ -32,6 +34,7 @@ import PageHeader from '@/components/admin/PageHeader'
|
|||||||
import { isFeedbackDismissed, showError, showPrompt, showSuccess } from '@/lib/feedback'
|
import { isFeedbackDismissed, showError, showPrompt, showSuccess } from '@/lib/feedback'
|
||||||
import {
|
import {
|
||||||
deleteAdminCloudtentaclesSource,
|
deleteAdminCloudtentaclesSource,
|
||||||
|
exchangeAdminKuaishouIndustryAuthorizationCode,
|
||||||
failAdminNinetyoneOrder,
|
failAdminNinetyoneOrder,
|
||||||
fetchAdminCloudtentaclesAsset,
|
fetchAdminCloudtentaclesAsset,
|
||||||
fetchAdminCloudtentaclesSkuList,
|
fetchAdminCloudtentaclesSkuList,
|
||||||
@@ -1069,7 +1072,10 @@ function KuaishouIndustryPanel({
|
|||||||
}) {
|
}) {
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const [exchanging, setExchanging] = useState(false)
|
||||||
|
const [authorizationCode, setAuthorizationCode] = useState('')
|
||||||
const source = config.source
|
const source = config.source
|
||||||
|
const authorizationUrl = buildIndustryAuthorizationUrl(source)
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
@@ -1099,6 +1105,51 @@ function KuaishouIndustryPanel({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function exchangeAuthorizationCode() {
|
||||||
|
const code = authorizationCode.trim()
|
||||||
|
if (!code) {
|
||||||
|
showError('请输入授权 code')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setExchanging(true)
|
||||||
|
try {
|
||||||
|
const saved = await saveAdminKuaishouIndustrySourceConfig(source)
|
||||||
|
onChange(saved.data)
|
||||||
|
const response = await exchangeAdminKuaishouIndustryAuthorizationCode({ code })
|
||||||
|
onChange(response.data)
|
||||||
|
setAuthorizationCode('')
|
||||||
|
showSuccess('授权 token 已保存')
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : '授权 code 换 token 失败')
|
||||||
|
} finally {
|
||||||
|
setExchanging(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyAuthorizationUrl() {
|
||||||
|
if (!authorizationUrl) {
|
||||||
|
showError('授权链接未生成')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(authorizationUrl)
|
||||||
|
showSuccess('授权链接已复制')
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : '复制授权链接失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAuthorizationUrl() {
|
||||||
|
if (!authorizationUrl) {
|
||||||
|
showError('授权链接未生成')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(authorizationUrl, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
|
||||||
function updateSource(patch: Partial<AdminKuaishouIndustrySourceConfig>) {
|
function updateSource(patch: Partial<AdminKuaishouIndustrySourceConfig>) {
|
||||||
onChange({
|
onChange({
|
||||||
...config,
|
...config,
|
||||||
@@ -1170,7 +1221,7 @@ function KuaishouIndustryPanel({
|
|||||||
<Button
|
<Button
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
loading={refreshing}
|
loading={refreshing}
|
||||||
disabled={saving}
|
disabled={saving || exchanging}
|
||||||
onClick={refreshToken}
|
onClick={refreshToken}
|
||||||
>
|
>
|
||||||
保存并刷新 token
|
保存并刷新 token
|
||||||
@@ -1179,7 +1230,7 @@ function KuaishouIndustryPanel({
|
|||||||
type="primary"
|
type="primary"
|
||||||
icon={<SaveOutlined />}
|
icon={<SaveOutlined />}
|
||||||
loading={saving}
|
loading={saving}
|
||||||
disabled={refreshing}
|
disabled={refreshing || exchanging}
|
||||||
onClick={saveConfig}
|
onClick={saveConfig}
|
||||||
>
|
>
|
||||||
保存配置
|
保存配置
|
||||||
@@ -1210,7 +1261,7 @@ function KuaishouIndustryPanel({
|
|||||||
onChange={(baseUrl) => updateSource({ baseUrl })}
|
onChange={(baseUrl) => updateSource({ baseUrl })}
|
||||||
/>
|
/>
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
label="授权 API"
|
label="授权页地址"
|
||||||
value={source.authBaseUrl}
|
value={source.authBaseUrl}
|
||||||
onChange={(authBaseUrl) => updateSource({ authBaseUrl })}
|
onChange={(authBaseUrl) => updateSource({ authBaseUrl })}
|
||||||
/>
|
/>
|
||||||
@@ -1224,6 +1275,74 @@ function KuaishouIndustryPanel({
|
|||||||
{renderSecretInput('messageSecret', 'messageSecret', source.messageSecretMasked)}
|
{renderSecretInput('messageSecret', 'messageSecret', source.messageSecretMasked)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
title="授权"
|
||||||
|
className="platform-section-gap"
|
||||||
|
extra={
|
||||||
|
<Space wrap>
|
||||||
|
<Button icon={<CopyOutlined />} disabled={!authorizationUrl} onClick={copyAuthorizationUrl}>
|
||||||
|
复制链接
|
||||||
|
</Button>
|
||||||
|
<Button icon={<LinkOutlined />} disabled={!authorizationUrl} onClick={openAuthorizationUrl}>
|
||||||
|
打开授权
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="platform-form-grid">
|
||||||
|
<LabeledInput
|
||||||
|
label="回调地址"
|
||||||
|
value={source.redirectUri}
|
||||||
|
onChange={(redirectUri) => updateSource({ redirectUri })}
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
label="scope"
|
||||||
|
value={source.scopes}
|
||||||
|
onChange={(scopes) => updateSource({ scopes })}
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
label="state"
|
||||||
|
value={source.authState}
|
||||||
|
onChange={(authState) => updateSource({ authState })}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography.Text type="secondary">授权链接</Typography.Text>
|
||||||
|
<Input readOnly value={authorizationUrl} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography.Text type="secondary">openId</Typography.Text>
|
||||||
|
<Input
|
||||||
|
value={source.openId}
|
||||||
|
onChange={(event) => updateSource({ openId: event.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography.Text type="secondary">已授权 scope</Typography.Text>
|
||||||
|
<Input
|
||||||
|
value={source.grantedScopes}
|
||||||
|
onChange={(event) => updateSource({ grantedScopes: event.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Space.Compact className="full-width platform-section-gap">
|
||||||
|
<Input
|
||||||
|
value={authorizationCode}
|
||||||
|
placeholder="code"
|
||||||
|
onChange={(event) => setAuthorizationCode(event.target.value)}
|
||||||
|
onPressEnter={exchangeAuthorizationCode}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={exchanging}
|
||||||
|
disabled={saving || refreshing}
|
||||||
|
onClick={exchangeAuthorizationCode}
|
||||||
|
>
|
||||||
|
换取 token
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card size="small" title="Token" className="platform-section-gap">
|
<Card size="small" title="Token" className="platform-section-gap">
|
||||||
<div className="platform-form-grid">
|
<div className="platform-form-grid">
|
||||||
{renderSecretInput('accessToken', 'accessToken', source.accessTokenMasked)}
|
{renderSecretInput('accessToken', 'accessToken', source.accessTokenMasked)}
|
||||||
@@ -1738,6 +1857,35 @@ function formatIndustryTokenCountdown(value: number | null) {
|
|||||||
return `${minutes} 分钟后过期`
|
return `${minutes} 分钟后过期`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildIndustryAuthorizationUrl(source: AdminKuaishouIndustrySourceConfig) {
|
||||||
|
if (!source.authBaseUrl || !source.appKey || !source.redirectUri || !source.scopes) {
|
||||||
|
return source.authorizationUrl || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(`${source.authBaseUrl.replace(/\/+$/, '')}/oauth/authorize`)
|
||||||
|
url.searchParams.set('app_id', source.appKey)
|
||||||
|
url.searchParams.set('redirect_uri', source.redirectUri)
|
||||||
|
url.searchParams.set('scope', normalizeIndustryScopeText(source.scopes))
|
||||||
|
url.searchParams.set('response_type', 'code')
|
||||||
|
if (source.authState) {
|
||||||
|
url.searchParams.set('state', source.authState)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.toString()
|
||||||
|
} catch {
|
||||||
|
return source.authorizationUrl || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeIndustryScopeText(value: string) {
|
||||||
|
return value
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
function MetricCard({ label, value, detail }: { label: string; value: string; detail?: string }) {
|
function MetricCard({ label, value, detail }: { label: string; value: string; detail?: string }) {
|
||||||
return (
|
return (
|
||||||
<Card className="metric-card">
|
<Card className="metric-card">
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { apiGet, apiPost } from '@/lib/http'
|
import { apiGet, apiPost } from '@/lib/http'
|
||||||
import type {
|
import type {
|
||||||
|
AdminKuaishouIndustryAuthorizationCodePayload,
|
||||||
AdminKuaishouIndustryConfigResponse,
|
AdminKuaishouIndustryConfigResponse,
|
||||||
|
AdminKuaishouIndustryExchangeCodeResponse,
|
||||||
AdminKuaishouIndustryRefreshTokenResponse,
|
AdminKuaishouIndustryRefreshTokenResponse,
|
||||||
AdminKuaishouIndustrySourceConfig,
|
AdminKuaishouIndustrySourceConfig,
|
||||||
} from '@/types/admin'
|
} from '@/types/admin'
|
||||||
@@ -26,3 +28,12 @@ export function refreshAdminKuaishouIndustryAccessToken() {
|
|||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function exchangeAdminKuaishouIndustryAuthorizationCode(
|
||||||
|
payload: AdminKuaishouIndustryAuthorizationCodePayload,
|
||||||
|
) {
|
||||||
|
return apiPost<AdminKuaishouIndustryExchangeCodeResponse>(
|
||||||
|
'/api/v1/admin/platform-config/kuaishou-industry-source/exchange-code',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export type {
|
|||||||
AdminKuaishouIndustrySourceConfig,
|
AdminKuaishouIndustrySourceConfig,
|
||||||
AdminKuaishouIndustryConfigResponse,
|
AdminKuaishouIndustryConfigResponse,
|
||||||
AdminKuaishouIndustryRefreshTokenResponse,
|
AdminKuaishouIndustryRefreshTokenResponse,
|
||||||
|
AdminKuaishouIndustryAuthorizationCodePayload,
|
||||||
|
AdminKuaishouIndustryExchangeCodeResponse,
|
||||||
AdminKuaishouFeifeiProductRule,
|
AdminKuaishouFeifeiProductRule,
|
||||||
AdminKuaishouFeifeiConfig,
|
AdminKuaishouFeifeiConfig,
|
||||||
AdminKuaishouFeifeiEffectiveConfig,
|
AdminKuaishouFeifeiEffectiveConfig,
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export type {
|
|||||||
AdminKuaishouIndustrySourceConfig,
|
AdminKuaishouIndustrySourceConfig,
|
||||||
AdminKuaishouIndustryConfigResponse,
|
AdminKuaishouIndustryConfigResponse,
|
||||||
AdminKuaishouIndustryRefreshTokenResponse,
|
AdminKuaishouIndustryRefreshTokenResponse,
|
||||||
|
AdminKuaishouIndustryAuthorizationCodePayload,
|
||||||
|
AdminKuaishouIndustryExchangeCodeResponse,
|
||||||
} from './kuaishou-industry'
|
} from './kuaishou-industry'
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ export interface AdminKuaishouIndustrySourceConfig {
|
|||||||
sendCallbackEnabled: boolean
|
sendCallbackEnabled: boolean
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
authBaseUrl: string
|
authBaseUrl: string
|
||||||
|
redirectUri: string
|
||||||
|
scopes: string
|
||||||
|
authState: string
|
||||||
|
authorizationUrl: string
|
||||||
appKey: string
|
appKey: string
|
||||||
appSecret: string
|
appSecret: string
|
||||||
appSecretMasked: string
|
appSecretMasked: string
|
||||||
@@ -30,6 +34,8 @@ export interface AdminKuaishouIndustrySourceConfig {
|
|||||||
refreshTokenExpiresAt: string
|
refreshTokenExpiresAt: string
|
||||||
accessTokenStatus: AdminKuaishouIndustryAccessTokenStatus
|
accessTokenStatus: AdminKuaishouIndustryAccessTokenStatus
|
||||||
accessTokenExpiresInSeconds: number | null
|
accessTokenExpiresInSeconds: number | null
|
||||||
|
openId: string
|
||||||
|
grantedScopes: string
|
||||||
sellerId: string
|
sellerId: string
|
||||||
provider: string
|
provider: string
|
||||||
platform: string
|
platform: string
|
||||||
@@ -48,3 +54,9 @@ export interface AdminKuaishouIndustryConfigResponse {
|
|||||||
export interface AdminKuaishouIndustryRefreshTokenResponse extends AdminKuaishouIndustryConfigResponse {
|
export interface AdminKuaishouIndustryRefreshTokenResponse extends AdminKuaishouIndustryConfigResponse {
|
||||||
refreshed: boolean
|
refreshed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminKuaishouIndustryAuthorizationCodePayload {
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdminKuaishouIndustryExchangeCodeResponse = AdminKuaishouIndustryRefreshTokenResponse
|
||||||
|
|||||||
Reference in New Issue
Block a user