ok
This commit is contained in:
@@ -13,7 +13,8 @@
|
||||
"dependencies": {
|
||||
"crypto-pro": "^2.3.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"marked": "^12.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
|
||||
+51
-9
@@ -2,21 +2,45 @@ import { useState, useEffect } from 'react'
|
||||
import { AuthPage } from './pages/AuthPage'
|
||||
import { CheckCodesPage } from './pages/CheckCodesPage'
|
||||
import { UploadUpdPage } from './pages/UploadUpdPage'
|
||||
import { setOnUnauthorized, logout as apiLogout } from './services/api'
|
||||
import { setOnUnauthorized, logout as apiLogout, getEnv, setEnv, getAuthStatus } from './services/api'
|
||||
import type { ApiEnv } from './types'
|
||||
|
||||
type Page = 'auth' | 'check' | 'upload'
|
||||
|
||||
export default function App() {
|
||||
const [page, setPage] = useState<Page>('auth')
|
||||
const [authenticated, setAuthenticated] = useState(false)
|
||||
const [env, setEnvState] = useState<ApiEnv>('sandbox')
|
||||
|
||||
useEffect(() => {
|
||||
setOnUnauthorized(() => {
|
||||
setAuthenticated(false)
|
||||
apiLogout().catch(() => {})
|
||||
setOnUnauthorized((failedEnv: ApiEnv) => {
|
||||
if (failedEnv === getEnv()) {
|
||||
setAuthenticated(false)
|
||||
}
|
||||
apiLogout(failedEnv).catch(() => {})
|
||||
})
|
||||
}, [])
|
||||
|
||||
async function handleEnvChange(newEnv: ApiEnv) {
|
||||
if (newEnv === 'prod' && env === 'sandbox') {
|
||||
const ok = window.confirm(
|
||||
'Вы переключаетесь на промышленный контур.\n' +
|
||||
'Все запросы будут отправляться к реальной системе «Честный знак».\n\n' +
|
||||
'Продолжить?'
|
||||
)
|
||||
if (!ok) return
|
||||
}
|
||||
|
||||
setEnv(newEnv)
|
||||
setEnvState(newEnv)
|
||||
setAuthenticated(false)
|
||||
setPage('auth')
|
||||
apiLogout(newEnv).catch(() => {})
|
||||
|
||||
const status = await getAuthStatus(newEnv).catch(() => ({ authenticated: false }))
|
||||
if (status.authenticated) setAuthenticated(true)
|
||||
}
|
||||
|
||||
function handleAuth() {
|
||||
setAuthenticated(true)
|
||||
setPage('check')
|
||||
@@ -24,7 +48,7 @@ export default function App() {
|
||||
|
||||
function handleLogout() {
|
||||
setAuthenticated(false)
|
||||
apiLogout().catch(() => {})
|
||||
apiLogout(getEnv()).catch(() => {})
|
||||
setPage('auth')
|
||||
}
|
||||
|
||||
@@ -54,7 +78,25 @@ export default function App() {
|
||||
Загрузка УПД
|
||||
</button>
|
||||
</nav>
|
||||
<div style={{ marginLeft: 'auto', fontSize: 13, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ marginLeft: 'auto', fontSize: 13, display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<label>
|
||||
<select
|
||||
value={env}
|
||||
onChange={e => handleEnvChange(e.target.value as ApiEnv)}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.15)',
|
||||
color: 'white',
|
||||
border: '1px solid rgba(255,255,255,0.3)',
|
||||
borderRadius: 4,
|
||||
padding: '3px 8px',
|
||||
cursor: 'pointer',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
<option value="sandbox" style={{ color: '#000' }}>🧪 Sandbox</option>
|
||||
<option value="prod" style={{ color: '#000' }}>🏭 Промышленный</option>
|
||||
</select>
|
||||
</label>
|
||||
{authenticated ? '✅ Авторизован' : '❌ Не авторизован'}
|
||||
{authenticated && (
|
||||
<button onClick={handleLogout}
|
||||
@@ -74,9 +116,9 @@ export default function App() {
|
||||
</header>
|
||||
|
||||
<main style={{ padding: 24, maxWidth: 960, margin: '0 auto' }}>
|
||||
{page === 'auth' && <AuthPage onAuth={handleAuth} />}
|
||||
{page === 'check' && <CheckCodesPage />}
|
||||
{page === 'upload' && <UploadUpdPage />}
|
||||
{page === 'auth' && <AuthPage key={env} onAuth={handleAuth} />}
|
||||
{page === 'check' && <CheckCodesPage key={env} />}
|
||||
{page === 'upload' && <UploadUpdPage key={env} />}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { getAuthStatus, getAuthKey, signIn } from '../services/api'
|
||||
import type { RequestError } from '../services/api'
|
||||
import { isCadesPluginAvailable, listCertificates, signWithCadesPlugin } from '../services/cadesplugin'
|
||||
import type { CertInfo } from '../services/cadesplugin'
|
||||
|
||||
@@ -14,6 +15,8 @@ export function AuthPage({ onAuth }: Props) {
|
||||
const [selectedThumbprint, setSelectedThumbprint] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [errorDebug, setErrorDebug] = useState<string | null>(null)
|
||||
const [debugExpanded, setDebugExpanded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
getAuthStatus().then(setStatus).catch(() => {})
|
||||
@@ -34,6 +37,8 @@ export function AuthPage({ onAuth }: Props) {
|
||||
async function handleSignIn() {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
setErrorDebug(null)
|
||||
setDebugExpanded(false)
|
||||
try {
|
||||
const { uuid, data } = await getAuthKey()
|
||||
const signature = await signWithCadesPlugin(data, selectedThumbprint || undefined)
|
||||
@@ -42,7 +47,11 @@ export function AuthPage({ onAuth }: Props) {
|
||||
setStatus(s)
|
||||
if (s.authenticated) onAuth()
|
||||
} catch (err) {
|
||||
setError(String(err instanceof Error ? err.message : err))
|
||||
const re = err as RequestError
|
||||
setError(re.message)
|
||||
if (re.debugInfo) {
|
||||
setErrorDebug(JSON.stringify(re.debugInfo, null, 2))
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -120,6 +129,41 @@ export function AuthPage({ onAuth }: Props) {
|
||||
{error && (
|
||||
<div style={{ color: '#c62828', fontSize: 14, marginTop: 8 }}>Ошибка: {error}</div>
|
||||
)}
|
||||
|
||||
{errorDebug && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<button
|
||||
onClick={() => setDebugExpanded(!debugExpanded)}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: '1px solid #b0bec5',
|
||||
borderRadius: 4,
|
||||
padding: '6px 12px',
|
||||
cursor: 'pointer',
|
||||
fontSize: 13,
|
||||
color: '#37474f',
|
||||
}}
|
||||
>
|
||||
{debugExpanded ? '▼ Скрыть лог запроса к API маркировки' : '▶ Показать лог запроса к API маркировки'}
|
||||
</button>
|
||||
{debugExpanded && (
|
||||
<pre style={{
|
||||
marginTop: 8,
|
||||
padding: 12,
|
||||
background: '#263238',
|
||||
color: '#e0e0e0',
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.5,
|
||||
overflowX: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
}}>
|
||||
{errorDebug}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 24, fontSize: 13, color: '#666' }}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { checkCodesPublic, checkCodesAuth } from '../services/api'
|
||||
import type { RequestError } from '../services/api'
|
||||
import { ResultsTable } from '../components/ResultsTable'
|
||||
import type { CodeResult } from '../types'
|
||||
|
||||
@@ -8,6 +9,8 @@ export function CheckCodesPage() {
|
||||
const [results, setResults] = useState<CodeResult[] | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [errorDebug, setErrorDebug] = useState<string | null>(null)
|
||||
const [debugExpanded, setDebugExpanded] = useState(false)
|
||||
|
||||
async function handleCheck(mode: 'public' | 'auth') {
|
||||
const codes = codesInput
|
||||
@@ -22,12 +25,18 @@ export function CheckCodesPage() {
|
||||
|
||||
setLoading(true)
|
||||
setError('')
|
||||
setErrorDebug(null)
|
||||
setDebugExpanded(false)
|
||||
try {
|
||||
const fn = mode === 'public' ? checkCodesPublic : checkCodesAuth
|
||||
const response = await fn(codes)
|
||||
setResults(response.results)
|
||||
} catch (err) {
|
||||
setError(String(err))
|
||||
const re = err as RequestError
|
||||
setError(re.message)
|
||||
if (re.debugInfo) {
|
||||
setErrorDebug(JSON.stringify(re.debugInfo, null, 2))
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -69,6 +78,41 @@ export function CheckCodesPage() {
|
||||
<div style={{ color: '#c62828', marginBottom: 12 }}>Ошибка: {error}</div>
|
||||
)}
|
||||
|
||||
{errorDebug && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<button
|
||||
onClick={() => setDebugExpanded(!debugExpanded)}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: '1px solid #b0bec5',
|
||||
borderRadius: 4,
|
||||
padding: '6px 12px',
|
||||
cursor: 'pointer',
|
||||
fontSize: 13,
|
||||
color: '#37474f',
|
||||
}}
|
||||
>
|
||||
{debugExpanded ? '▼ Скрыть лог запроса к API маркировки' : '▶ Показать лог запроса к API маркировки'}
|
||||
</button>
|
||||
{debugExpanded && (
|
||||
<pre style={{
|
||||
marginTop: 8,
|
||||
padding: 12,
|
||||
background: '#263238',
|
||||
color: '#e0e0e0',
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.5,
|
||||
overflowX: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
}}>
|
||||
{errorDebug}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{results && (
|
||||
<div>
|
||||
<div style={{ marginBottom: 12, fontSize: 14 }}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { uploadUpd } from '../services/api'
|
||||
import type { RequestError } from '../services/api'
|
||||
import { ResultsTable } from '../components/ResultsTable'
|
||||
import type { UpdUploadResponse } from '../types'
|
||||
|
||||
@@ -10,6 +11,8 @@ export function UploadUpdPage() {
|
||||
const [response, setResponse] = useState<UpdUploadResponse | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [errorDebug, setErrorDebug] = useState<string | null>(null)
|
||||
const [debugExpanded, setDebugExpanded] = useState(false)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
@@ -27,11 +30,17 @@ export function UploadUpdPage() {
|
||||
if (!file) return
|
||||
setLoading(true)
|
||||
setError('')
|
||||
setErrorDebug(null)
|
||||
setDebugExpanded(false)
|
||||
try {
|
||||
const result = await uploadUpd(file)
|
||||
setResponse(result)
|
||||
} catch (err) {
|
||||
setError(String(err))
|
||||
const re = err as RequestError
|
||||
setError(re.message)
|
||||
if (re.debugInfo) {
|
||||
setErrorDebug(JSON.stringify(re.debugInfo, null, 2))
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -95,6 +104,41 @@ export function UploadUpdPage() {
|
||||
<div style={{ color: '#c62828', marginTop: 12 }}>Ошибка: {error}</div>
|
||||
)}
|
||||
|
||||
{errorDebug && (
|
||||
<div style={{ marginTop: 12, marginBottom: 16 }}>
|
||||
<button
|
||||
onClick={() => setDebugExpanded(!debugExpanded)}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: '1px solid #b0bec5',
|
||||
borderRadius: 4,
|
||||
padding: '6px 12px',
|
||||
cursor: 'pointer',
|
||||
fontSize: 13,
|
||||
color: '#37474f',
|
||||
}}
|
||||
>
|
||||
{debugExpanded ? '▼ Скрыть лог запроса к API маркировки' : '▶ Показать лог запроса к API маркировки'}
|
||||
</button>
|
||||
{debugExpanded && (
|
||||
<pre style={{
|
||||
marginTop: 8,
|
||||
padding: 12,
|
||||
background: '#263238',
|
||||
color: '#e0e0e0',
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.5,
|
||||
overflowX: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
}}>
|
||||
{errorDebug}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{response && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div style={{
|
||||
|
||||
@@ -1,85 +1,106 @@
|
||||
import type { AuthStatus, CheckCodesResponse, UpdUploadResponse } from '../types'
|
||||
import type { AuthStatus, CheckCodesResponse, UpdUploadResponse, ApiErrorResponse, ApiEnv } from '../types'
|
||||
|
||||
const BASE_URL = ''
|
||||
|
||||
let onUnauthorized: (() => void) | null = null
|
||||
let currentEnv: ApiEnv = 'sandbox'
|
||||
let onUnauthorized: ((env: ApiEnv) => void) | null = null
|
||||
|
||||
export function setOnUnauthorized(cb: () => void): void {
|
||||
export interface RequestError extends Error {
|
||||
debugInfo?: ApiErrorResponse['debugInfo']
|
||||
}
|
||||
|
||||
export function setOnUnauthorized(cb: (env: ApiEnv) => void): void {
|
||||
onUnauthorized = cb
|
||||
}
|
||||
|
||||
async function request<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
export function getEnv(): ApiEnv {
|
||||
return currentEnv
|
||||
}
|
||||
|
||||
export function setEnv(env: ApiEnv): void {
|
||||
currentEnv = env
|
||||
}
|
||||
|
||||
function qs(env?: ApiEnv): string {
|
||||
return `?env=${env || currentEnv}`
|
||||
}
|
||||
|
||||
async function handleError(res: Response, env: ApiEnv): Promise<never> {
|
||||
if (res.status === 401 && onUnauthorized) {
|
||||
onUnauthorized(env)
|
||||
}
|
||||
const body: ApiErrorResponse = await res.json().catch(() => ({ error: res.statusText }))
|
||||
const err = new Error(body.error || `HTTP ${res.status}`) as RequestError
|
||||
err.debugInfo = body.debugInfo
|
||||
throw err
|
||||
}
|
||||
|
||||
async function request<T>(url: string, options?: RequestInit & { env?: ApiEnv }): Promise<T> {
|
||||
const reqEnv = options?.env || currentEnv
|
||||
const headers: Record<string, string> = {}
|
||||
if (!(options?.body instanceof FormData)) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
if (options?.headers) {
|
||||
Object.assign(headers, options.headers)
|
||||
Object.assign(headers, options.headers as Record<string, string>)
|
||||
}
|
||||
|
||||
const res = await fetch(`${BASE_URL}${url}`, {
|
||||
const urlWithEnv = `${BASE_URL}${url}${qs(reqEnv)}`
|
||||
const res = await fetch(urlWithEnv, {
|
||||
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}`)
|
||||
}
|
||||
if (!res.ok) await handleError(res, reqEnv)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function getAuthKey(): Promise<{ uuid: string; data: string }> {
|
||||
return request('/auth/key')
|
||||
export async function getAuthKey(env?: ApiEnv): Promise<{ uuid: string; data: string }> {
|
||||
return request('/auth/key', { env })
|
||||
}
|
||||
|
||||
export async function signIn(uuid: string, data: string): Promise<{ token: string }> {
|
||||
export async function signIn(uuid: string, data: string, env?: ApiEnv): Promise<{ token: string }> {
|
||||
return request('/auth/simpleSignIn', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ uuid, data }),
|
||||
env,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getAuthStatus(): Promise<AuthStatus> {
|
||||
return request('/auth/status')
|
||||
export async function getAuthStatus(env?: ApiEnv): Promise<AuthStatus> {
|
||||
return request('/auth/status', { env })
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
await request('/auth/logout', { method: 'POST' })
|
||||
export async function logout(env?: ApiEnv): Promise<void> {
|
||||
await request('/auth/logout', { method: 'POST', env })
|
||||
}
|
||||
|
||||
export async function checkCodesPublic(codes: string[]): Promise<CheckCodesResponse> {
|
||||
export async function checkCodesPublic(codes: string[], env?: ApiEnv): Promise<CheckCodesResponse> {
|
||||
return request('/api/check-codes/public', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ codes }),
|
||||
env,
|
||||
})
|
||||
}
|
||||
|
||||
export async function checkCodesAuth(codes: string[]): Promise<CheckCodesResponse> {
|
||||
export async function checkCodesAuth(codes: string[], env?: ApiEnv): Promise<CheckCodesResponse> {
|
||||
return request('/api/check-codes/auth', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ codes }),
|
||||
env,
|
||||
})
|
||||
}
|
||||
|
||||
export async function uploadUpd(file: File): Promise<UpdUploadResponse> {
|
||||
export async function uploadUpd(file: File, env?: ApiEnv): Promise<UpdUploadResponse> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return request('/api/upload-upd', { method: 'POST', body: formData, env })
|
||||
}
|
||||
|
||||
const res = await fetch(`${BASE_URL}/api/upload-upd`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
export async function getPlansList(): Promise<{ name: string; path: string; size: number }[]> {
|
||||
return request('/api/plans')
|
||||
}
|
||||
|
||||
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}`)
|
||||
}
|
||||
|
||||
return res.json()
|
||||
export async function getPlanContent(filename: string): Promise<{ name: string; content: string }> {
|
||||
return request(`/api/plans/${filename}`)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type ApiEnv = 'sandbox' | 'prod'
|
||||
|
||||
export interface AuthStatus {
|
||||
authenticated: boolean
|
||||
tokenExpiresAt: string | null
|
||||
@@ -31,6 +33,25 @@ export interface UpdDocument {
|
||||
fileName: string
|
||||
}
|
||||
|
||||
export interface ApiErrorLog {
|
||||
request: {
|
||||
method: string
|
||||
url: string
|
||||
headers: Record<string, string>
|
||||
body: unknown
|
||||
}
|
||||
response: {
|
||||
statusCode: number
|
||||
headers: Record<string, string>
|
||||
body: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApiErrorResponse {
|
||||
error: string
|
||||
debugInfo?: ApiErrorLog
|
||||
}
|
||||
|
||||
export interface UpdUploadResponse {
|
||||
document: UpdDocument
|
||||
codesFound: number
|
||||
|
||||
Reference in New Issue
Block a user