mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-01 00:27:30 +00:00
feat: extend Server-Timing to authenticated user web APIs
Mirror the Admin UI Server-Timing opt-in for user-facing pages so authenticated callers can inspect total/app/db/redis/deps metrics on session, profile, keys, usage, payment, and related user APIs. - Collect when X-User-UI-Request=1 or path is on the user allowlist - Emit for non-admin only on allowlisted paths (header is not auth) - Exclude payment public/webhook surfaces - Mark matching SPA requests and allow the new CORS request header
This commit is contained in:
@@ -2,7 +2,10 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
ADMIN_UI_REQUEST_HEADER,
|
||||
USER_UI_REQUEST_HEADER,
|
||||
isUserTimingAPIPath,
|
||||
shouldMarkAdminUIRequest,
|
||||
shouldMarkUserUIRequest,
|
||||
} from '@/api/adminUIRequest'
|
||||
|
||||
describe('Admin UI request marker', () => {
|
||||
@@ -36,3 +39,62 @@ describe('Admin UI request marker', () => {
|
||||
expect(shouldMarkAdminUIRequest(requestURL, pagePath)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('User UI request marker', () => {
|
||||
it('uses the stable request header name', () => {
|
||||
expect(USER_UI_REQUEST_HEADER).toBe('X-User-UI-Request')
|
||||
})
|
||||
|
||||
it.each([
|
||||
'/auth/me',
|
||||
'/auth/revoke-all-sessions',
|
||||
'/auth/oauth/bind-token',
|
||||
'/user',
|
||||
'/user/profile',
|
||||
'/user/password',
|
||||
'/user/notify-email/send-code',
|
||||
'/user/totp/status',
|
||||
'/user/aff',
|
||||
'/user/platform-quotas',
|
||||
'/keys',
|
||||
'/keys/12',
|
||||
'/groups/available',
|
||||
'/groups/rates',
|
||||
'/channels/available',
|
||||
'/usage',
|
||||
'/usage/stats',
|
||||
'/usage/dashboard/snapshot-v2',
|
||||
'/announcements',
|
||||
'/announcements/3/read',
|
||||
'/redeem',
|
||||
'/redeem/history',
|
||||
'/subscriptions',
|
||||
'/subscriptions/active',
|
||||
'/channel-monitors',
|
||||
'/channel-monitors/9/status',
|
||||
'/payment/config',
|
||||
'/payment/plans',
|
||||
'/payment/orders',
|
||||
'/payment/orders/my',
|
||||
'/api/v1/auth/me',
|
||||
'/api/v1/keys?page=1',
|
||||
'https://api.example.test/api/v1/payment/orders/1',
|
||||
])('marks user timing API %s', (requestURL) => {
|
||||
expect(shouldMarkUserUIRequest(requestURL)).toBe(true)
|
||||
expect(isUserTimingAPIPath(requestURL)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'/auth/login',
|
||||
'/settings/public',
|
||||
'/admin/users',
|
||||
'/groups',
|
||||
'/channels',
|
||||
'/payment/public/orders/verify',
|
||||
'/payment/webhook/stripe',
|
||||
'/api/v1/payment/public/orders/resolve',
|
||||
'',
|
||||
])('does not mark non-user timing API %s', (requestURL) => {
|
||||
expect(shouldMarkUserUIRequest(requestURL)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -170,6 +170,58 @@ describe('API Client', () => {
|
||||
const config = adapter.mock.calls[0][0]
|
||||
expect(config.headers.get('X-Admin-UI-Request')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('用户侧 timing API 自动带 User UI 标记', async () => {
|
||||
const adapter = vi.fn().mockResolvedValue({
|
||||
status: 200,
|
||||
data: { code: 0, data: {} },
|
||||
headers: {},
|
||||
config: {},
|
||||
statusText: 'OK',
|
||||
})
|
||||
apiClient.defaults.adapter = adapter
|
||||
|
||||
await apiClient.get('/auth/me')
|
||||
|
||||
const config = adapter.mock.calls[0][0]
|
||||
expect(config.headers.get('X-User-UI-Request')).toBe('1')
|
||||
expect(config.headers.get('X-Admin-UI-Request')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('支付用户 API 带 User UI 标记,公开支付 API 不带', async () => {
|
||||
const adapter = vi.fn().mockResolvedValue({
|
||||
status: 200,
|
||||
data: { code: 0, data: {} },
|
||||
headers: {},
|
||||
config: {},
|
||||
statusText: 'OK',
|
||||
})
|
||||
apiClient.defaults.adapter = adapter
|
||||
|
||||
await apiClient.get('/payment/plans')
|
||||
expect(adapter.mock.calls[0][0].headers.get('X-User-UI-Request')).toBe('1')
|
||||
|
||||
await apiClient.post('/payment/public/orders/verify', {})
|
||||
expect(adapter.mock.calls[1][0].headers.get('X-User-UI-Request')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('管理页调用共享 API 时同时带 Admin 与 User UI 标记', async () => {
|
||||
window.history.replaceState({}, '', '/admin/dashboard')
|
||||
const adapter = vi.fn().mockResolvedValue({
|
||||
status: 200,
|
||||
data: { code: 0, data: {} },
|
||||
headers: {},
|
||||
config: {},
|
||||
statusText: 'OK',
|
||||
})
|
||||
apiClient.defaults.adapter = adapter
|
||||
|
||||
await apiClient.get('/keys')
|
||||
|
||||
const config = adapter.mock.calls[0][0]
|
||||
expect(config.headers.get('X-Admin-UI-Request')).toBe('1')
|
||||
expect(config.headers.get('X-User-UI-Request')).toBe('1')
|
||||
})
|
||||
})
|
||||
|
||||
// --- 响应拦截器 ---
|
||||
|
||||
Reference in New Issue
Block a user