Add admin compliance acknowledgement gate

This commit is contained in:
shaw
2026-06-10 14:16:51 +08:00
parent c32e29bab0
commit 0acf00c4a1
29 changed files with 1173 additions and 15 deletions
+47
View File
@@ -143,6 +143,53 @@ describe('API Client', () => {
})
)
})
it('部署与运营合规未确认时广播事件且保留登录态', async () => {
localStorage.setItem('auth_token', 'admin-token')
const listener = vi.fn()
window.addEventListener('admin-compliance-required', listener)
const adapter = vi.fn().mockRejectedValue({
response: {
status: 423,
data: {
code: 'ADMIN_COMPLIANCE_ACK_REQUIRED',
message: 'administrator compliance acknowledgement is required',
metadata: {
version: 'v2026.06.10',
document_path_zh: 'docs/legal/admin-compliance.zh.md',
document_path_en: 'docs/legal/admin-compliance.en.md',
},
},
},
config: {
url: '/admin/users',
headers: { Authorization: 'Bearer admin-token' },
},
code: 'ERR_BAD_REQUEST',
})
apiClient.defaults.adapter = adapter
await expect(apiClient.get('/admin/users')).rejects.toEqual(
expect.objectContaining({
status: 423,
code: 'ADMIN_COMPLIANCE_ACK_REQUIRED',
metadata: expect.objectContaining({
version: 'v2026.06.10',
}),
})
)
expect(listener).toHaveBeenCalledTimes(1)
expect((listener.mock.calls[0][0] as CustomEvent).detail).toEqual(
expect.objectContaining({
version: 'v2026.06.10',
})
)
expect(localStorage.getItem('auth_token')).toBe('admin-token')
window.removeEventListener('admin-compliance-required', listener)
})
})
// --- 401 Token 刷新 ---
+42
View File
@@ -0,0 +1,42 @@
import { apiClient } from '@/api/client'
export interface AdminComplianceAcknowledgement {
version: string
document_zh: string
document_en: string
admin_user_id: number
ip_address?: string
user_agent?: string
accepted_at: string
}
export interface AdminComplianceStatus {
required: boolean
version: string
document_path_zh: string
document_path_en: string
document_url_zh: string
document_url_en: string
ack_phrase_zh: string
ack_phrase_en: string
acknowledgement?: AdminComplianceAcknowledgement
}
export interface AcceptAdminComplianceRequest {
phrase: string
language: string
}
export const adminComplianceAPI = {
async getStatus(): Promise<AdminComplianceStatus> {
const { data } = await apiClient.get<AdminComplianceStatus>('/admin/compliance')
return data
},
async accept(payload: AcceptAdminComplianceRequest): Promise<AdminComplianceStatus> {
const { data } = await apiClient.post<AdminComplianceStatus>('/admin/compliance/accept', payload)
return data
}
}
export default adminComplianceAPI
+5 -2
View File
@@ -31,6 +31,7 @@ import channelMonitorTemplateAPI from './channelMonitorTemplate'
import adminPaymentAPI from './payment'
import affiliatesAPI from './affiliates'
import riskControlAPI from './riskControl'
import adminComplianceAPI from './compliance'
/**
* Unified admin API object for convenient access
@@ -63,7 +64,8 @@ export const adminAPI = {
channelMonitorTemplate: channelMonitorTemplateAPI,
payment: adminPaymentAPI,
affiliates: affiliatesAPI,
riskControl: riskControlAPI
riskControl: riskControlAPI,
compliance: adminComplianceAPI
}
export {
@@ -94,7 +96,8 @@ export {
channelMonitorTemplateAPI,
adminPaymentAPI,
affiliatesAPI,
riskControlAPI
riskControlAPI,
adminComplianceAPI
}
export default adminAPI
+17
View File
@@ -148,6 +148,23 @@ apiClient.interceptors.response.use(
})
}
if (status === 423 && apiData.code === 'ADMIN_COMPLIANCE_ACK_REQUIRED') {
try {
window.dispatchEvent(new CustomEvent('admin-compliance-required', {
detail: apiData.metadata || {}
}))
} catch {
// ignore event failures
}
return Promise.reject({
status,
code: apiData.code,
message: apiData.message || error.message,
metadata: apiData.metadata,
})
}
// 401: Try to refresh the token if we have a refresh token
// This handles TOKEN_EXPIRED, INVALID_TOKEN, TOKEN_REVOKED, etc.
if (status === 401 && !originalRequest._retry) {