mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 05:56:29 +00:00
fix(frontend): refine token refresh controls
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "node --test --experimental-strip-types src/app/theme.test.ts src/components/templates/template-config.test.ts",
|
"test": "node --test --experimental-strip-types src/app/theme.test.ts src/components/templates/template-config.test.ts src/views/dashboard-license.test.ts",
|
||||||
"typecheck": "vue-tsc -b",
|
"typecheck": "vue-tsc -b",
|
||||||
"lint": "eslint . --fix",
|
"lint": "eslint . --fix",
|
||||||
"lint:check": "eslint .",
|
"lint:check": "eslint .",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { useRouter } from '@/app/router'
|
|||||||
import StateBlock from '@/components/StateBlock.vue'
|
import StateBlock from '@/components/StateBlock.vue'
|
||||||
import { alertClass, cardClass, inputClass, sectionHeaderClass, toneClass } from '@/components/ui'
|
import { alertClass, cardClass, inputClass, sectionHeaderClass, toneClass } from '@/components/ui'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import {
|
import {
|
||||||
cronLabel,
|
cronLabel,
|
||||||
extractErrorMessage,
|
extractErrorMessage,
|
||||||
@@ -31,6 +32,11 @@ import {
|
|||||||
statusLabel,
|
statusLabel,
|
||||||
statusTone,
|
statusTone,
|
||||||
} from '@/utils/format'
|
} from '@/utils/format'
|
||||||
|
import {
|
||||||
|
canRefreshAuthorization,
|
||||||
|
formatAuthorizationExpiryTooltip,
|
||||||
|
formatRemainingDays,
|
||||||
|
} from './dashboard-license'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuth()
|
const auth = useAuth()
|
||||||
@@ -71,6 +77,9 @@ const tokenDetail = computed(() => {
|
|||||||
if (tokenStatus.value.expiring_soon) return 'Token 即将过期,建议准备刷新授权。'
|
if (tokenStatus.value.expiring_soon) return 'Token 即将过期,建议准备刷新授权。'
|
||||||
return `业务 Token 正常,${tokenStatus.value.days_until_expiry ?? '未知'} 天后过期。`
|
return `业务 Token 正常,${tokenStatus.value.days_until_expiry ?? '未知'} 天后过期。`
|
||||||
})
|
})
|
||||||
|
const remainingDaysLabel = computed(() => formatRemainingDays(tokenStatus.value?.days_until_expiry))
|
||||||
|
const expiryTooltip = computed(() => formatAuthorizationExpiryTooltip(tokenStatus.value))
|
||||||
|
const canRefreshToken = computed(() => canRefreshAuthorization(tokenStatus.value))
|
||||||
const needsEmail = computed(() => !auth.state.user?.email)
|
const needsEmail = computed(() => !auth.state.user?.email)
|
||||||
const needsPassword = computed(() => auth.state.user?.has_password === false)
|
const needsPassword = computed(() => auth.state.user?.has_password === false)
|
||||||
|
|
||||||
@@ -295,12 +304,23 @@ onMounted(load)
|
|||||||
<div class="grid gap-3 p-4 text-sm">
|
<div class="grid gap-3 p-4 text-sm">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-muted-foreground">剩余</span>
|
<span class="text-muted-foreground">剩余</span>
|
||||||
<span class="font-medium">
|
<TooltipProvider v-if="expiryTooltip">
|
||||||
{{
|
<Tooltip>
|
||||||
tokenStatus?.days_until_expiry == null
|
<TooltipTrigger as-child>
|
||||||
? '未知'
|
<span
|
||||||
: `${tokenStatus.days_until_expiry} 天`
|
class="cursor-help font-medium underline decoration-muted-foreground/40 underline-offset-4"
|
||||||
}}
|
:title="expiryTooltip"
|
||||||
|
>
|
||||||
|
{{ remainingDaysLabel }}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{{ expiryTooltip }}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<span v-else class="font-medium">
|
||||||
|
{{ remainingDaysLabel }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@@ -309,7 +329,8 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-muted-foreground">{{ tokenDetail }}</div>
|
<div class="text-sm text-muted-foreground">{{ tokenDetail }}</div>
|
||||||
<Button
|
<Button
|
||||||
:variant="tokenStatus?.is_valid ? 'outline' : 'default'"
|
:variant="canRefreshToken ? 'default' : 'outline'"
|
||||||
|
:disabled="!canRefreshToken"
|
||||||
type="button"
|
type="button"
|
||||||
@click="router.navigate('/login')"
|
@click="router.navigate('/login')"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
import type { TokenStatus } from '@/api'
|
||||||
|
import {
|
||||||
|
canRefreshAuthorization,
|
||||||
|
formatAuthorizationExpiryTooltip,
|
||||||
|
formatRemainingDays,
|
||||||
|
} from './dashboard-license.ts'
|
||||||
|
|
||||||
|
test('formats authorization expiry tooltip with concrete expiration time', () => {
|
||||||
|
const token: TokenStatus = {
|
||||||
|
is_valid: true,
|
||||||
|
jwt_exp: '',
|
||||||
|
expires_at: Date.UTC(2026, 4, 6, 16, 30, 12) / 1000,
|
||||||
|
days_until_expiry: 0,
|
||||||
|
expiring_soon: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
formatAuthorizationExpiryTooltip(token, 'zh-CN', 'UTC'),
|
||||||
|
'过期时间:2026-05-06 16:30:12',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('omits authorization expiry tooltip when expiration timestamp is missing', () => {
|
||||||
|
assert.equal(formatAuthorizationExpiryTooltip(null), null)
|
||||||
|
assert.equal(
|
||||||
|
formatAuthorizationExpiryTooltip({
|
||||||
|
is_valid: false,
|
||||||
|
jwt_exp: '',
|
||||||
|
expires_at: null,
|
||||||
|
days_until_expiry: null,
|
||||||
|
expiring_soon: false,
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('allows QR refresh only for expired or invalid authorization', () => {
|
||||||
|
assert.equal(canRefreshAuthorization(null), false)
|
||||||
|
assert.equal(
|
||||||
|
canRefreshAuthorization({
|
||||||
|
is_valid: true,
|
||||||
|
jwt_exp: '',
|
||||||
|
expires_at: 1_778_111_412,
|
||||||
|
days_until_expiry: 0,
|
||||||
|
expiring_soon: true,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
canRefreshAuthorization({
|
||||||
|
is_valid: false,
|
||||||
|
jwt_exp: '',
|
||||||
|
expires_at: null,
|
||||||
|
days_until_expiry: null,
|
||||||
|
expiring_soon: false,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('formats remaining days label consistently', () => {
|
||||||
|
assert.equal(formatRemainingDays(null), '未知')
|
||||||
|
assert.equal(formatRemainingDays(0), '0 天')
|
||||||
|
assert.equal(formatRemainingDays(12), '12 天')
|
||||||
|
})
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { TokenStatus } from '@/api'
|
||||||
|
|
||||||
|
export function formatRemainingDays(days?: number | null) {
|
||||||
|
return days == null ? '未知' : `${days} 天`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canRefreshAuthorization(token?: TokenStatus | null) {
|
||||||
|
return token?.is_valid === false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAuthorizationExpiryTooltip(
|
||||||
|
token?: TokenStatus | null,
|
||||||
|
_locale?: string,
|
||||||
|
timeZone?: string,
|
||||||
|
) {
|
||||||
|
if (!token?.expires_at) return null
|
||||||
|
|
||||||
|
const expiresAt = new Date(token.expires_at * 1000)
|
||||||
|
if (Number.isNaN(expiresAt.getTime())) return null
|
||||||
|
|
||||||
|
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hourCycle: 'h23',
|
||||||
|
timeZone,
|
||||||
|
})
|
||||||
|
.formatToParts(expiresAt)
|
||||||
|
.reduce<Record<string, string>>((values, part) => {
|
||||||
|
if (part.type !== 'literal') values[part.type] = part.value
|
||||||
|
return values
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return `过期时间:${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second}`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user