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")