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:
kislovdm
2026-07-08 16:53:35 +03:00
parent d2819ff03c
commit 70eac33c8c
19 changed files with 501 additions and 240 deletions
+26 -2
View File
@@ -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}`)