diff --git a/apps/frontend/src/views/admin/AdminUsersView.vue b/apps/frontend/src/views/admin/AdminUsersView.vue index f9a7835..7494ca6 100644 --- a/apps/frontend/src/views/admin/AdminUsersView.vue +++ b/apps/frontend/src/views/admin/AdminUsersView.vue @@ -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)
{{ user.email || '未设置邮箱' }} {{ user.email_verified ? '邮箱已验证' : '邮箱未验证' }} + {{ + userAuthorizationSummary(user).label + }} {{ formatDateTime(user.created_at) }}
diff --git a/apps/frontend/src/views/dashboard-license.test.ts b/apps/frontend/src/views/dashboard-license.test.ts index d5ea8a4..124d3be 100644 --- a/apps/frontend/src/views/dashboard-license.test.ts +++ b/apps/frontend/src/views/dashboard-license.test.ts @@ -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', + }) +}) diff --git a/apps/frontend/src/views/dashboard-license.ts b/apps/frontend/src/views/dashboard-license.ts index 3e054f7..b30795c 100644 --- a/apps/frontend/src/views/dashboard-license.ts +++ b/apps/frontend/src/views/dashboard-license.ts @@ -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', + } +} diff --git a/tests/test_frontend_architecture.py b/tests/test_frontend_architecture.py index 1ce478e..6e3e7da 100644 --- a/tests/test_frontend_architecture.py +++ b/tests/test_frontend_architecture.py @@ -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")