import { ApiRequestError, getSession, requestJson } from './api.uts' import { agreementScope, agreementVersionValid, agreementRecordIdValid, agreementRoute, optionalConsentScope, sameConsentSession } from './agreements-route.uts' export { agreementScope, agreementRoute, optionalConsentScope, sameConsentSession } from './agreements-route.uts' export type Agreement = { revisionId: string, code: string, version: string, scope: string, title: string, content: string, contentHash: string, effectiveAt: string } export type Consent = { consentId: string, scope: string, agreementCode: string, agreementVersion: string, contentHash: string, grantedAt: string, revokedAt: string, canRevoke: boolean } export type ConsentPage = { items: Consent[], page: number, pageSize: number, total: number, hasMore: boolean } const scopes = ['account-terms', 'account-privacy', 'documents', 'data'] const validHash = (value: string): boolean => /^[a-f0-9]{64}$/.test(value) const validTime = (value: string): boolean => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value) && !isNaN(Date.parse(value)) const validKey = (value: string): boolean => /^[A-Za-z0-9_-]{16,64}$/.test(value) export function consentRequestId(): string { return 'consent_' + Date.now().toString() + '_' + Math.floor(Math.random() * 1000000000).toString() } export function emptyAgreement(): Agreement { return { revisionId: '', code: '', version: '', scope: '', title: '', content: '', contentHash: '', effectiveAt: '' } } export function agreementUnavailable(error: any | null): boolean { return error instanceof ApiRequestError && error.status == 404 && error.errorCode == 'AGREEMENT_NOT_AVAILABLE' } export function agreementChanged(error: any | null): boolean { return error instanceof ApiRequestError && error.status == 409 && error.errorCode == 'AGREEMENT_CHANGED' } export const consentTitle = (code: string): string => { if (code == 'terms') return '用户协议' if (code == 'privacy') return '隐私政策' if (code == 'document-use') return '资料使用协议' if (code == 'data-authorization') return '数据授权协议' return '协议' } export const consentTime = (value: string): string => value.length == 0 ? '' : value.replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC').replace('Z', ' UTC') function validateAgreement(item: Agreement): void { if (!agreementRecordIdValid(item.revisionId) || agreementScope(item.code).length == 0 || item.scope != agreementScope(item.code) || !agreementVersionValid(item.version) || !validHash(item.contentHash) || item.title.trim().length == 0 || item.content.trim().length == 0 || !validTime(item.effectiveAt)) throw new Error('协议响应不完整,请重新加载') } function agreementView(data: UTSJSONObject): Agreement { const item: Agreement = { revisionId: data.getString('revisionId') ?? '', code: data.getString('code') ?? '', version: data.getString('version') ?? '', scope: data.getString('scope') ?? '', title: data.getString('title') ?? '', content: data.getString('content') ?? '', contentHash: data.getString('contentHash') ?? '', effectiveAt: data.getString('effectiveAt') ?? '' } validateAgreement(item) return item } function consentView(data: UTSJSONObject): Consent { const revokedValue = data['revokedAt'] if (revokedValue != null && typeof revokedValue != 'string') throw new Error('同意记录撤销时间无效') const revokedAt = revokedValue == null ? '' : revokedValue as string const flag = data.getBoolean('canRevoke') const item: Consent = { consentId: data.getString('consentId') ?? '', scope: data.getString('scope') ?? '', agreementCode: data.getString('agreementCode') ?? '', agreementVersion: data.getString('agreementVersion') ?? '', contentHash: data.getString('contentHash') ?? '', grantedAt: data.getString('grantedAt') ?? '', revokedAt, canRevoke: flag ?? false } if (!agreementRecordIdValid(item.consentId) || agreementScope(item.agreementCode).length == 0 || item.scope != agreementScope(item.agreementCode) || !agreementVersionValid(item.agreementVersion) || !validHash(item.contentHash) || !validTime(item.grantedAt) || (revokedAt.length > 0 && !validTime(revokedAt)) || flag == null || (item.canRevoke && (!optionalConsentScope(item.scope) || revokedAt.length > 0))) throw new Error('同意记录响应不完整,请重新加载') return item } function sessionOwner(): UTSJSONObject { const session = getSession() if (session == null) throw new Error('请先登录后管理同意记录') return session } function checkOwner(owner: UTSJSONObject): void { const current = getSession() if (!sameConsentSession(owner.getString('token') ?? '', owner.getString('userId') ?? '', current?.getString('token') ?? '', current?.getString('userId') ?? '')) throw new Error('登录状态已变化,请重新操作') } export async function fetchAgreement(code: string, version: string = ''): Promise { if (agreementRoute(code, version).length == 0) throw new Error('协议参数无效,请返回重新选择') const query: UTSJSONObject = {} if (version.length > 0) query['version'] = version const item = agreementView(await requestJson('/agreements/' + code, 'GET', query)) if (item.code != code || (version.length > 0 && item.version != version)) throw new Error('协议版本不一致,请重新加载') return item } export async function grantConsent(agreement: Agreement, requestId: string): Promise { const owner = sessionOwner() validateAgreement(agreement) if (!validKey(requestId)) throw new Error('同意请求标识无效') const item = consentView(await requestJson('/consents', 'POST', { scope: agreement.scope, agreementCode: agreement.code, agreementVersion: agreement.version, requestId })) checkOwner(owner) if (item.scope != agreement.scope || item.agreementCode != agreement.code || item.agreementVersion != agreement.version || item.contentHash != agreement.contentHash || (item.revokedAt.length > 0 && !optionalConsentScope(item.scope))) throw new Error('同意回执未能核对,请用原请求重试') return item } export async function fetchConsents(scope: string = '', page: number = 1, pageSize: number = 20): Promise { const owner = sessionOwner() if ((scope.length > 0 && !scopes.includes(scope)) || !Number.isInteger(page) || page < 1 || page > 1000 || !Number.isInteger(pageSize) || pageSize < 1 || pageSize > 50) throw new Error('同意记录查询参数无效') const query: UTSJSONObject = { page: page.toString(), pageSize: pageSize.toString() } if (scope.length > 0) query['scope'] = scope const data = await requestJson('/consents', 'GET', query) checkOwner(owner) const raw = data['items']; const total = data.getNumber('total'); const hasMore = data.getBoolean('hasMore') if (raw == null || !Array.isArray(raw) || data.getNumber('page') != page || data.getNumber('pageSize') != pageSize || total == null || !Number.isInteger(total) || total < 0 || hasMore == null) throw new Error('同意记录分页响应无效') const rows = raw as UTSJSONObject[] const items = rows.map((row: UTSJSONObject): Consent => { if (row == null || typeof row != 'object' || Array.isArray(row)) throw new Error('同意记录格式无效') const item = consentView(row) if (scope.length > 0 && item.scope != scope) throw new Error('同意记录范围不一致') return item }) return { items, page, pageSize, total, hasMore } } export async function revokeConsent(consent: Consent, requestId: string): Promise { const owner = sessionOwner() if (!agreementRecordIdValid(consent.consentId) || !optionalConsentScope(consent.scope) || !consent.canRevoke || consent.revokedAt.length > 0 || !validKey(requestId)) throw new Error('仅可撤销当前有效的可选授权') const item = consentView(await requestJson('/consents/' + consent.consentId + '/revoke', 'POST', { requestId })) checkOwner(owner) if (item.consentId != consent.consentId || item.scope != consent.scope || item.agreementCode != consent.agreementCode || item.agreementVersion != consent.agreementVersion || item.contentHash != consent.contentHash || item.grantedAt != consent.grantedAt || item.revokedAt.length == 0 || item.canRevoke) throw new Error('撤销回执未能核对,请用原请求重试') return item } export function showAgreementNotice(title: string): void { let code = '' if (title == '用户协议') code = 'terms' else if (title == '隐私政策' || title == '隐私说明') code = 'privacy' else if (title == '资料使用协议') code = 'document-use' else if (title == '数据授权协议') code = 'data-authorization' const url = agreementRoute(code) if (url.length > 0) uni.navigateTo({ url }) }