feat(api): implement chestny-znak web app with True API integration
- Fastify backend with auth (CryptoPro CAdES-BES), KM check, UPD parsing - React frontend with auth page, code check, XML upload, results table - Client-side signing via crypto-pro 2.3.0 (browser plugin) - Token caching with auto-refresh, retry with exponential backoff - Input validation schemas, file size limits, global 401 interceptor - Docker compose with backend + frontend (nginx) services
This commit is contained in:
@@ -2,11 +2,27 @@ import type { AuthStatus, CheckCodesResponse, UpdUploadResponse } from '../types
|
||||
|
||||
const BASE_URL = ''
|
||||
|
||||
let onUnauthorized: (() => void) | null = null
|
||||
|
||||
export function setOnUnauthorized(cb: () => void): void {
|
||||
onUnauthorized = cb
|
||||
}
|
||||
|
||||
async function request<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (options?.headers) {
|
||||
Object.assign(headers, options.headers)
|
||||
}
|
||||
|
||||
const res = await fetch(`${BASE_URL}${url}`, {
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
headers,
|
||||
...options,
|
||||
})
|
||||
|
||||
if (res.status === 401 && onUnauthorized) {
|
||||
onUnauthorized()
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: res.statusText }))
|
||||
throw new Error(err.error || `HTTP ${res.status}`)
|
||||
@@ -19,7 +35,7 @@ export async function getAuthKey(): Promise<{ uuid: string; data: string }> {
|
||||
}
|
||||
|
||||
export async function signIn(uuid: string, data: string): Promise<{ token: string }> {
|
||||
return request('/auth/signin', {
|
||||
return request('/auth/simpleSignIn', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ uuid, data }),
|
||||
})
|
||||
@@ -29,6 +45,10 @@ export async function getAuthStatus(): Promise<AuthStatus> {
|
||||
return request('/auth/status')
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
await request('/auth/logout', { method: 'POST' })
|
||||
}
|
||||
|
||||
export async function checkCodesPublic(codes: string[]): Promise<CheckCodesResponse> {
|
||||
return request('/api/check-codes/public', {
|
||||
method: 'POST',
|
||||
@@ -52,6 +72,10 @@ export async function uploadUpd(file: File): Promise<UpdUploadResponse> {
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (res.status === 401 && onUnauthorized) {
|
||||
onUnauthorized()
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: res.statusText }))
|
||||
throw new Error(err.error || `HTTP ${res.status}`)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
getUserCertificates,
|
||||
createAttachedSignature,
|
||||
} from 'crypto-pro'
|
||||
|
||||
export interface CertInfo {
|
||||
name: string
|
||||
thumbprint: string
|
||||
issuer: string
|
||||
validTo: string
|
||||
valid: boolean
|
||||
}
|
||||
|
||||
export async function isCadesPluginAvailable(): Promise<boolean> {
|
||||
try {
|
||||
await getUserCertificates(true)
|
||||
return true
|
||||
} catch (err) {
|
||||
if (err instanceof Error && (
|
||||
err.message.includes('undefined') || err.message.includes('not defined')
|
||||
)) return false
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function listCertificates(): Promise<CertInfo[]> {
|
||||
const certs = await getUserCertificates(true)
|
||||
const now = Date.now()
|
||||
return certs.map(c => {
|
||||
const validTo = new Date(c.validTo)
|
||||
return {
|
||||
name: c.name || 'Без имени',
|
||||
thumbprint: c.thumbprint,
|
||||
issuer: c.issuerName,
|
||||
validTo: c.validTo,
|
||||
valid: validTo.getTime() > now,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function signWithCadesPlugin(dataBase64: string, thumbprint?: string): Promise<string> {
|
||||
const certs = await getUserCertificates(true)
|
||||
const now = Date.now()
|
||||
const validCerts = certs.filter(c => c.name && new Date(c.validTo).getTime() > now)
|
||||
|
||||
if (validCerts.length === 0) {
|
||||
throw new Error('Нет доступных сертификатов с действующим сроком')
|
||||
}
|
||||
|
||||
let tp = thumbprint
|
||||
if (!tp) {
|
||||
tp = validCerts[0].thumbprint
|
||||
} else {
|
||||
const found = validCerts.find(c => c.thumbprint.toUpperCase() === tp!.toUpperCase())
|
||||
if (!found) throw new Error(`Сертификат с отпечатком ${tp} не найден или просрочен`)
|
||||
}
|
||||
|
||||
return createAttachedSignature(tp, dataBase64)
|
||||
}
|
||||
Reference in New Issue
Block a user