feat(frontend): show user credential expiry

This commit is contained in:
2026-05-06 22:13:54 +08:00
parent ce55cfc6b3
commit 3fceb1516c
4 changed files with 63 additions and 0 deletions
@@ -13,6 +13,7 @@ import {
} from '@/components/ui'
import { Button } from '@/components/ui/button'
import { extractErrorMessage, formatDateTime } from '@/utils/format'
import { formatUserAuthorizationSummary } from '../dashboard-license'
const loading = ref(true)
const error = ref('')
@@ -33,6 +34,10 @@ function requiresUnverifiedEmailOverride(
return 'requires_override' in result && result.warning_code === 'UNVERIFIED_EMAIL'
}
function userAuthorizationSummary(user: User) {
return formatUserAuthorizationSummary(user.jwt_exp)
}
async function load() {
loading.value = true
error.value = ''
@@ -162,6 +167,9 @@ onMounted(load)
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-sm text-muted-foreground">
<span>{{ user.email || '未设置邮箱' }}</span>
<span>{{ user.email_verified ? '邮箱已验证' : '邮箱未验证' }}</span>
<span :class="toneClass(userAuthorizationSummary(user).tone)">{{
userAuthorizationSummary(user).label
}}</span>
<span>{{ formatDateTime(user.created_at) }}</span>
</div>
</div>
@@ -4,6 +4,7 @@ import type { TokenStatus } from '@/api'
import {
canRefreshAuthorization,
formatAuthorizationExpiryTooltip,
formatUserAuthorizationSummary,
formatRemainingDays,
} from './dashboard-license.ts'
@@ -65,3 +66,22 @@ test('formats remaining days label consistently', () => {
assert.equal(formatRemainingDays(0), '0 天')
assert.equal(formatRemainingDays(12), '12 天')
})
test('formats user authorization summary from jwt expiration', () => {
assert.deepEqual(formatUserAuthorizationSummary('0'), {
label: '未绑定凭证',
tone: 'neutral',
})
assert.deepEqual(formatUserAuthorizationSummary('1000', 2000), {
label: '凭证过期',
tone: 'danger',
})
assert.deepEqual(formatUserAuthorizationSummary(String(2000 + 2 * 24 * 60 * 60), 2000), {
label: '2 天后过期',
tone: 'warning',
})
assert.deepEqual(formatUserAuthorizationSummary(String(2000 + 9 * 24 * 60 * 60), 2000), {
label: '9 天后过期',
tone: 'success',
})
})
@@ -1,4 +1,12 @@
import type { TokenStatus } from '@/api'
import type { Tone } from '@/components/ui'
const SECONDS_PER_DAY = 24 * 60 * 60
export interface UserAuthorizationSummary {
label: string
tone: Tone
}
export function formatRemainingDays(days?: number | null) {
return days == null ? '未知' : `${days}`
@@ -36,3 +44,23 @@ export function formatAuthorizationExpiryTooltip(
return `过期时间:${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second}`
}
export function formatUserAuthorizationSummary(
jwtExp?: string | null,
nowSeconds = Math.floor(Date.now() / 1000),
): UserAuthorizationSummary {
const expiresAt = Number(jwtExp)
if (!jwtExp || jwtExp === '0' || !Number.isFinite(expiresAt) || expiresAt <= 0) {
return { label: '未绑定凭证', tone: 'neutral' }
}
if (expiresAt <= nowSeconds) {
return { label: '凭证过期', tone: 'danger' }
}
const remainingDays = Math.ceil((expiresAt - nowSeconds) / SECONDS_PER_DAY)
return {
label: `${remainingDays} 天后过期`,
tone: remainingDays <= 7 ? 'warning' : 'success',
}
}
+7
View File
@@ -90,6 +90,13 @@ def test_frontend_admin_approval_policy_warnings_are_visible() -> None:
assert "未验证邮箱审批警告" not in email_settings
def test_frontend_admin_users_show_authorization_summary() -> None:
admin_users = (SRC_ROOT / "views" / "admin" / "AdminUsersView.vue").read_text(encoding="utf-8")
assert "formatUserAuthorizationSummary" in admin_users
assert "jwt_exp" in admin_users
def test_frontend_replaces_starter_component() -> None:
app = (SRC_ROOT / "App.vue").read_text(encoding="utf-8")