mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-05 17:07:46 +08:00
299 lines
8.9 KiB
TypeScript
299 lines
8.9 KiB
TypeScript
import type { ProviderKeyStatusSnapshot } from '@/api/endpoints/types/statusSnapshot'
|
|
import { getOAuthExpiresCountdown, type OAuthStatusInfo } from '@/composables/useCountdownTimer'
|
|
import {
|
|
classifyAccountBlockLabel,
|
|
cleanAccountBlockReason,
|
|
isAccountLevelBlockReason,
|
|
isRefreshFailedReason,
|
|
} from './accountBlock'
|
|
import {
|
|
canRefreshOAuthCredential,
|
|
isOAuthManagedCredential,
|
|
type ProviderKeyAuthCarrier,
|
|
} from './providerKeyAuth'
|
|
|
|
export interface ProviderKeyStatusCarrier extends ProviderKeyAuthCarrier {
|
|
oauth_expires_at?: number | null
|
|
oauth_invalid_at?: number | null // compatibility only
|
|
oauth_invalid_reason?: string | null // compatibility only
|
|
account_status_label?: string | null // compatibility only
|
|
account_status_reason?: string | null // compatibility only
|
|
account_status_blocked?: boolean | null // compatibility only
|
|
status_snapshot?: ProviderKeyStatusSnapshot | null
|
|
}
|
|
|
|
export interface AccountStatusDisplay {
|
|
code: string
|
|
label: string | null
|
|
reason: string | null
|
|
blocked: boolean
|
|
}
|
|
|
|
function normalizeText(value: unknown): string | null {
|
|
if (typeof value !== 'string') return null
|
|
const text = value.trim()
|
|
return text || null
|
|
}
|
|
|
|
function buildLegacyAccountStatus(input: ProviderKeyStatusCarrier): AccountStatusDisplay {
|
|
const explicitLabel = normalizeText(input.account_status_label)
|
|
if (input.account_status_blocked && explicitLabel) {
|
|
return {
|
|
code: 'legacy',
|
|
label: explicitLabel,
|
|
reason: normalizeText(input.account_status_reason),
|
|
blocked: true,
|
|
}
|
|
}
|
|
|
|
const invalidReason = normalizeText(input.oauth_invalid_reason)
|
|
if (!invalidReason || !isAccountLevelBlockReason(invalidReason)) {
|
|
return { code: 'ok', label: null, reason: null, blocked: false }
|
|
}
|
|
|
|
const cleaned = cleanAccountBlockReason(invalidReason) || invalidReason
|
|
return {
|
|
code: 'legacy',
|
|
label: classifyAccountBlockLabel(cleaned || invalidReason),
|
|
reason: normalizeText(cleaned),
|
|
blocked: true,
|
|
}
|
|
}
|
|
|
|
export function getAccountStatusDisplay(input: ProviderKeyStatusCarrier): AccountStatusDisplay {
|
|
const snapshot = input.status_snapshot?.account
|
|
if (snapshot) {
|
|
return {
|
|
code: normalizeText(snapshot.code) || 'ok',
|
|
label: normalizeText(snapshot.label),
|
|
reason: normalizeText(snapshot.reason),
|
|
blocked: Boolean(snapshot.blocked),
|
|
}
|
|
}
|
|
return buildLegacyAccountStatus(input)
|
|
}
|
|
|
|
function getSnapshotOAuthState(
|
|
input: ProviderKeyStatusCarrier,
|
|
tick: number,
|
|
): OAuthStatusInfo | null {
|
|
const oauth = input.status_snapshot?.oauth
|
|
if (!oauth) return null
|
|
|
|
const code = normalizeText(oauth.code) || 'none'
|
|
const expiresAt = oauth.expires_at ?? input.oauth_expires_at ?? null
|
|
const reason = normalizeText(oauth.reason)
|
|
|
|
if (code === 'reauth_required') {
|
|
const countdown = expiresAt == null ? null : getOAuthExpiresCountdown(expiresAt, tick, null, null)
|
|
return {
|
|
text: countdown?.text ? `续期失败 ${countdown.text}` : '续期失败',
|
|
isExpired: false,
|
|
isExpiringSoon: countdown?.isExpiringSoon ?? false,
|
|
isInvalid: false,
|
|
invalidReason: reason || undefined,
|
|
requiresReauth: true,
|
|
usableUntilExpiry: true,
|
|
}
|
|
}
|
|
|
|
if (code === 'invalid') {
|
|
return {
|
|
text: '已失效',
|
|
isExpired: false,
|
|
isExpiringSoon: false,
|
|
isInvalid: true,
|
|
invalidReason: reason || undefined,
|
|
}
|
|
}
|
|
|
|
if (code === 'expired') {
|
|
return { text: '已过期', isExpired: true, isExpiringSoon: false, isInvalid: false }
|
|
}
|
|
|
|
if (code === 'check_failed') {
|
|
if (expiresAt == null) return null
|
|
return getOAuthExpiresCountdown(expiresAt, tick, null, null)
|
|
}
|
|
|
|
if (expiresAt == null) return null
|
|
return getOAuthExpiresCountdown(expiresAt, tick, null, null)
|
|
}
|
|
|
|
function refreshFailureAccessTokenStillUsable(expiresAt: number | null | undefined): boolean {
|
|
return typeof expiresAt === 'number' && expiresAt > Math.floor(Date.now() / 1000)
|
|
}
|
|
|
|
function getReauthRequiredOAuthState(
|
|
expiresAt: number | null | undefined,
|
|
tick: number,
|
|
reason: string,
|
|
): OAuthStatusInfo {
|
|
const countdown = expiresAt == null ? null : getOAuthExpiresCountdown(expiresAt, tick, null, null)
|
|
return {
|
|
text: countdown?.text ? `续期失败 ${countdown.text}` : '续期失败',
|
|
isExpired: false,
|
|
isExpiringSoon: countdown?.isExpiringSoon ?? false,
|
|
isInvalid: false,
|
|
invalidReason: reason,
|
|
requiresReauth: true,
|
|
usableUntilExpiry: true,
|
|
}
|
|
}
|
|
|
|
function getLegacyOAuthState(
|
|
input: ProviderKeyStatusCarrier,
|
|
tick: number,
|
|
): OAuthStatusInfo | null {
|
|
if (!isOAuthManagedCredential(input)) return null
|
|
if (!input.oauth_expires_at && !input.oauth_invalid_at && !input.oauth_invalid_reason) return null
|
|
|
|
const rawReason = normalizeText(input.oauth_invalid_reason)
|
|
if (
|
|
rawReason
|
|
&& isRefreshFailedReason(rawReason)
|
|
&& refreshFailureAccessTokenStillUsable(input.oauth_expires_at)
|
|
) {
|
|
return getReauthRequiredOAuthState(input.oauth_expires_at, tick, rawReason)
|
|
}
|
|
|
|
if (rawReason && isAccountLevelBlockReason(rawReason) && !isRefreshFailedReason(rawReason)) {
|
|
if (!input.oauth_expires_at) return null
|
|
return getOAuthExpiresCountdown(input.oauth_expires_at, tick, null, null)
|
|
}
|
|
|
|
return getOAuthExpiresCountdown(
|
|
input.oauth_expires_at,
|
|
tick,
|
|
input.oauth_invalid_at,
|
|
input.oauth_invalid_reason,
|
|
)
|
|
}
|
|
|
|
function getOAuthStatusSeverity(status: OAuthStatusInfo | null): number {
|
|
if (!status) return 0
|
|
if (status.isInvalid) return 3
|
|
if (status.isExpired) return 2
|
|
if (status.requiresReauth) return 2
|
|
return 1
|
|
}
|
|
|
|
function mergeOAuthStatusDisplay(
|
|
snapshotStatus: OAuthStatusInfo | null,
|
|
legacyStatus: OAuthStatusInfo | null,
|
|
): OAuthStatusInfo | null {
|
|
if (getOAuthStatusSeverity(legacyStatus) > getOAuthStatusSeverity(snapshotStatus)) {
|
|
return legacyStatus
|
|
}
|
|
|
|
if (
|
|
snapshotStatus?.isInvalid
|
|
&& !snapshotStatus.invalidReason
|
|
&& legacyStatus?.isInvalid
|
|
&& legacyStatus.invalidReason
|
|
) {
|
|
return {
|
|
...snapshotStatus,
|
|
invalidReason: legacyStatus.invalidReason,
|
|
}
|
|
}
|
|
|
|
return snapshotStatus ?? legacyStatus
|
|
}
|
|
|
|
function isOAuthCredentialWithoutRefreshToken(input: ProviderKeyStatusCarrier): boolean {
|
|
return isOAuthManagedCredential(input) && input.oauth_temporary === true
|
|
}
|
|
|
|
function getMissingRefreshTokenStatus(): OAuthStatusInfo {
|
|
return {
|
|
text: '未添加',
|
|
isExpired: false,
|
|
isExpiringSoon: false,
|
|
isInvalid: false,
|
|
}
|
|
}
|
|
|
|
export function getOAuthStatusDisplay(
|
|
input: ProviderKeyStatusCarrier,
|
|
tick: number,
|
|
): OAuthStatusInfo | null {
|
|
return mergeOAuthStatusDisplay(
|
|
getSnapshotOAuthState(input, tick),
|
|
getLegacyOAuthState(input, tick),
|
|
)
|
|
}
|
|
|
|
export function getOAuthStatusDisplayWithFallback(
|
|
input: ProviderKeyStatusCarrier,
|
|
tick: number,
|
|
): OAuthStatusInfo | null {
|
|
if (isOAuthCredentialWithoutRefreshToken(input)) {
|
|
return getMissingRefreshTokenStatus()
|
|
}
|
|
|
|
const status = getOAuthStatusDisplay(input, tick)
|
|
if (status) return status
|
|
if (!isOAuthManagedCredential(input)) return null
|
|
|
|
return {
|
|
text: '有效期未知',
|
|
isExpired: false,
|
|
isExpiringSoon: false,
|
|
isInvalid: false,
|
|
}
|
|
}
|
|
|
|
export function getOAuthStatusTitle(
|
|
input: ProviderKeyStatusCarrier,
|
|
tick: number,
|
|
): string {
|
|
if (isOAuthCredentialWithoutRefreshToken(input)) {
|
|
return 'Refresh Token 未添加,无法自动刷新'
|
|
}
|
|
|
|
const status = getOAuthStatusDisplay(input, tick)
|
|
if (!status) {
|
|
return isOAuthManagedCredential(input) ? 'Token 有效期未知' : ''
|
|
}
|
|
if (status.isInvalid) {
|
|
const reason = normalizeText(status.invalidReason)
|
|
return reason ? `Token 已失效: ${reason}` : 'Token 已失效'
|
|
}
|
|
const snapshotCode = normalizeText(input.status_snapshot?.oauth?.code)
|
|
if (snapshotCode === 'reauth_required' || status.requiresReauth) {
|
|
const reason = normalizeText(status.invalidReason)
|
|
return reason
|
|
? `Refresh Token 续期失败,当前 Access Token 未到期仍可使用: ${reason}`
|
|
: 'Refresh Token 续期失败,当前 Access Token 未到期仍可使用'
|
|
}
|
|
if (status.isExpired) {
|
|
return 'Access Token 已过期,等待自动续期'
|
|
}
|
|
return `Token 剩余有效期: ${status.text}`
|
|
}
|
|
|
|
export function getOAuthRefreshButtonTitle(
|
|
input: ProviderKeyStatusCarrier,
|
|
tick: number,
|
|
): string {
|
|
if (isOAuthManagedCredential(input) && !canRefreshOAuthCredential(input)) {
|
|
if (input.oauth_temporary === true) {
|
|
return '仅 Access Token 导入,无法自动刷新,到期后需要重新导入'
|
|
}
|
|
return '当前 OAuth 凭据无法自动刷新,到期后需要重新导入'
|
|
}
|
|
|
|
const status = getOAuthStatusDisplay(input, tick)
|
|
if (status?.isInvalid || status?.isExpired || status?.requiresReauth) {
|
|
return '重新授权'
|
|
}
|
|
return '刷新 Token'
|
|
}
|
|
|
|
export function getAccountStatusTitle(input: ProviderKeyStatusCarrier): string {
|
|
const account = getAccountStatusDisplay(input)
|
|
if (!account.blocked || !account.label) return ''
|
|
return account.reason ? `${account.label}: ${account.reason}` : account.label
|
|
}
|