From 5c8a0c8abb610877a45c3db7de6889ed9da1c09e Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 13 Jul 2026 09:23:24 +0000 Subject: [PATCH] ok --- backend/src/config.ts | 6 +- backend/src/routes/auth.ts | 17 +- backend/src/routes/cises.ts | 2 +- backend/src/services/auth-service.ts | 47 ++++- backend/src/services/cises-service.ts | 236 ++++++++++++++++---------- backend/src/types/index.ts | 28 ++- frontend/src/pages/AuthPage.tsx | 83 ++++++++- frontend/src/pages/CheckCodesPage.tsx | 11 +- frontend/src/services/api.ts | 14 +- frontend/src/types/index.ts | 7 +- ware.md | 69 ++++++++ 11 files changed, 405 insertions(+), 115 deletions(-) create mode 100644 ware.md diff --git a/backend/src/config.ts b/backend/src/config.ts index 4f6bb30..7081cf1 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -18,9 +18,9 @@ export const config = { corsOrigin: process.env.CORS_ORIGIN || '*', trueApi: { - authPath: '/auth', - cisesInfoPath: '/cises/info', - codesCheckPath: '/codes/check', + authPath: 'auth', + cisesInfoPath: 'cises/info', + codesCheckPath: 'codes/check', batchSize: 1000, tokenTTL: 10 * 60 * 60 * 1000, maxRetries: 3, diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index cd04786..1b09e40 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -1,6 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { TrueApiEnv } from '../config.js' -import { getAuthKey, signIn, clearToken, getAuthStatus } from '../services/auth-service.js' +import { getAuthKey, signIn, clearToken, getAuthStatus, setTokenDirect } from '../services/auth-service.js' import { AppError, extractDebugInfo } from '../services/cises-service.js' const VALID_ENVS = ['sandbox', 'prod'] @@ -18,6 +18,7 @@ const signInSchema = { properties: { uuid: { type: 'string' }, data: { type: 'string' }, + inn: { type: 'string' }, }, }, } @@ -43,11 +44,12 @@ export async function authRoutes(app: FastifyInstance): Promise { } }) - app.post<{ Body: { uuid: string; data: string } }>('/auth/simpleSignIn', { schema: signInSchema }, async (request, reply) => { + app.post<{ Body: { uuid: string; data: string; inn?: string } }>('/auth/simpleSignIn', { schema: signInSchema }, async (request, reply) => { const env = parseEnv(request.query as Record) try { - const { uuid, data } = request.body - const result = await signIn(env, { uuid, data }) + const { uuid, data, inn } = request.body + const result = await signIn(env, { uuid, data, inn }) + request.log.info({ env, token: result.token }, 'auth: token received') return result } catch (err) { if (err instanceof AppError) { @@ -74,4 +76,11 @@ export async function authRoutes(app: FastifyInstance): Promise { clearToken(env) return { success: true } }) + + app.post<{ Body: { token: string } }>('/auth/setToken', async (request) => { + const env = parseEnv(request.query as Record) + setTokenDirect(env, request.body.token) + request.log.info({ env, tokenPrefix: request.body.token.slice(0, 20) + '...' }, 'auth: token set manually') + return { success: true } + }) } diff --git a/backend/src/routes/cises.ts b/backend/src/routes/cises.ts index c594a4b..dd61fec 100644 --- a/backend/src/routes/cises.ts +++ b/backend/src/routes/cises.ts @@ -35,7 +35,7 @@ export async function cisesRoutes(app: FastifyInstance): Promise { app.post<{ Body: CheckCodesRequest }>('/api/check-codes/auth', { schema: checkCodesSchema }, async (request, reply) => { const env = parseEnv(request.query as Record) try { - return await checkCodesAuth(env, request.body) + return await checkCodesAuth(env, request.body, (msg, data) => request.log.error(data, msg)) } catch (err) { if (err instanceof AppError) { return reply.status(err.statusCode).send({ diff --git a/backend/src/services/auth-service.ts b/backend/src/services/auth-service.ts index a9ebf86..691773f 100644 --- a/backend/src/services/auth-service.ts +++ b/backend/src/services/auth-service.ts @@ -1,17 +1,50 @@ import got from 'got' +import type { Response } from 'got' import { config, envUrls } from '../config.js' import type { TrueApiEnv } from '../config.js' import { getToken, setToken, invalidateToken, hasToken, getTokenTTL } from './token-cache.js' import { AppError, extractDebugInfo } from './cises-service.js' import type { AuthKeyResponse, AuthSignInRequest, AuthSignInResponse } from '../types/index.js' -function authClient(env: TrueApiEnv) { +function loggedClient(prefixUrl: string) { return got.extend({ - prefixUrl: envUrls[env].baseUrl + config.trueApi.authPath, + prefixUrl, responseType: 'json', + hooks: { + beforeRequest: [ + (options) => { + const headers = { ...options.headers } as Record + if (headers.authorization) { + headers.authorization = 'Bearer ***' + } + console.log(JSON.stringify({ + type: 'trueapi_request', + method: options.method, + url: String(options.url || ''), + headers, + body: options.json || options.body || null, + })) + }, + ], + afterResponse: [ + (response: Response) => { + console.log(JSON.stringify({ + type: 'trueapi_response', + statusCode: response.statusCode, + url: response.requestUrl, + body: response.body, + })) + return response + }, + ], + }, }) } +function authClient(env: TrueApiEnv) { + return loggedClient(envUrls[env].baseUrl + config.trueApi.authPath) +} + export async function getAuthKey(env: TrueApiEnv): Promise { try { const { body } = await authClient(env).get('key') @@ -25,8 +58,12 @@ export async function getAuthKey(env: TrueApiEnv): Promise { export async function signIn(env: TrueApiEnv, body: AuthSignInRequest): Promise { try { + const requestBody: Record = { uuid: body.uuid, data: body.data } + if (body.inn) { + requestBody.inn = body.inn + } const { body: response } = await authClient(env).post('simpleSignIn', { - json: body, + json: requestBody, }) setToken(env, response.token) return response @@ -41,6 +78,10 @@ export function getCachedToken(env: TrueApiEnv): string | undefined { return getToken(env) } +export function setTokenDirect(env: TrueApiEnv, token: string): void { + setToken(env, token) +} + export function clearToken(env: TrueApiEnv): void { invalidateToken(env) } diff --git a/backend/src/services/cises-service.ts b/backend/src/services/cises-service.ts index 780ef84..febd9e4 100644 --- a/backend/src/services/cises-service.ts +++ b/backend/src/services/cises-service.ts @@ -1,22 +1,56 @@ import got from 'got' +import type { Response } from 'got' import { config, envUrls } from '../config.js' import type { TrueApiEnv } from '../config.js' import { getCachedToken, clearToken } from './auth-service.js' import type { - CisInfoResponse, + CisInfoResponseItem, PublicCheckResponse, CheckCodesRequest, CheckCodesResponse, SingleCodeResult, + ApiDebugInfo, } from '../types/index.js' -function apiClient(env: TrueApiEnv) { +function loggedClient(prefixUrl: string) { return got.extend({ - prefixUrl: envUrls[env].baseUrl, + prefixUrl, responseType: 'json', + hooks: { + beforeRequest: [ + (options) => { + const headers = { ...options.headers } as Record + if (headers.authorization) { + headers.authorization = 'Bearer ***' + } + console.log(JSON.stringify({ + type: 'trueapi_request', + method: options.method, + url: String(options.url || ''), + headers, + body: options.json || options.body || null, + })) + }, + ], + afterResponse: [ + (response: Response) => { + console.log(JSON.stringify({ + type: 'trueapi_response', + statusCode: response.statusCode, + url: response.requestUrl, + body: response.body, + })) + return response + }, + ], + }, }) } +function apiClient(env: TrueApiEnv) { + return loggedClient(envUrls[env].baseUrl) +} + export async function checkCodesPublic(env: TrueApiEnv, body: CheckCodesRequest): Promise { const results: SingleCodeResult[] = [] const queue = [...body.codes] @@ -25,9 +59,9 @@ export async function checkCodesPublic(env: TrueApiEnv, body: CheckCodesRequest) while (queue.length > 0) { const code = queue.shift()! try { - const { body: response } = await got.get( - `${envUrls[env].publicCheckUrl}?code=${encodeURIComponent(code)}`, - { responseType: 'json', timeout: { request: 10000 } } + const { body: response } = await loggedClient(envUrls[env].publicCheckUrl).get( + `?code=${encodeURIComponent(code)}`, + { timeout: { request: 10000 } } ) results.push({ code: response.code, @@ -59,9 +93,14 @@ function getToken(env: TrueApiEnv): string { return token } -export async function checkCodesAuth(env: TrueApiEnv, body: CheckCodesRequest): Promise { +export async function checkCodesAuth( + env: TrueApiEnv, + body: CheckCodesRequest, + log?: (msg: string, data?: Record) => void, +): Promise { const token = getToken(env) const results: SingleCodeResult[] = [] + let debugInfo: ApiDebugInfo | undefined try { await processBatch(env, body.codes, token, results, 0) @@ -70,6 +109,14 @@ export async function checkCodesAuth(env: TrueApiEnv, body: CheckCodesRequest): clearToken(env) throw err } + if (err instanceof AppError) { + debugInfo = err.debugInfo + if (log) log('Auth API error (non-401)', { error: err.message, debugInfo }) + } else { + debugInfo = extractDebugInfo(err) + if (log) log('Auth API error (non-401)', { error: String(err), debugInfo }) + } + for (const code of body.codes) { if (!results.find(r => r.code === code)) { results.push({ code, found: false, valid: false, status: 'ERROR', error: 'Auth API error' }) @@ -77,20 +124,46 @@ export async function checkCodesAuth(env: TrueApiEnv, body: CheckCodesRequest): } } - return buildResponse(results) + return buildResponse(results, debugInfo) } -export interface ApiDebugInfo { - request: { - method: string - url: string - headers: Record - body: unknown +function buildResponse(results: SingleCodeResult[], debugInfo?: ApiDebugInfo): CheckCodesResponse { + return { + results, + total: results.length, + validCount: results.filter(r => r.valid).length, + invalidCount: results.filter(r => !r.valid && !r.error).length, + errorCount: results.filter(r => !!r.error).length, + debugInfo, } - response: { - statusCode: number - headers: Record - body: unknown +} + +function extractRequestHeaders(options: Record | undefined): Record { + const reqHeaders = options?.headers as Record | undefined + const clean: Record = {} + if (reqHeaders) { + for (const [k, v] of Object.entries(reqHeaders)) { + clean[k] = k.toLowerCase() === 'authorization' ? 'Bearer ***' : String(v) + } + } + return clean +} + +export function extractDebugInfoFromResponse(response: Response): ApiDebugInfo { + const req = response.request as unknown as Record | undefined + const opts = req?.options as Record | undefined + return { + request: { + method: String(opts?.method || 'POST'), + url: String(response.requestUrl || response.url || 'unknown'), + headers: extractRequestHeaders(opts), + body: (opts?.json || opts?.body) ?? null, + }, + response: { + statusCode: response.statusCode, + headers: (response.headers || {}) as Record, + body: response.body ?? null, + }, } } @@ -99,33 +172,37 @@ export function extractDebugInfo(err: unknown): ApiDebugInfo | undefined { const gotErr = err as Record const response = gotErr.response as Record | undefined const options = gotErr.options as Record | undefined - if (!response) return undefined - const reqHeaders = options?.headers as Record | undefined - const cleanHeaders: Record = {} - if (reqHeaders) { - for (const [k, v] of Object.entries(reqHeaders)) { - if (k.toLowerCase() === 'authorization') { - cleanHeaders[k] = 'Bearer ***' - } else { - cleanHeaders[k] = String(v) - } - } + const code = gotErr.code ? String(gotErr.code) : undefined + const message = gotErr.message ? String(gotErr.message) : undefined + + const cleanHeaders = extractRequestHeaders(options) + + let reqUrl = 'unknown' + if (response?.requestUrl) { + reqUrl = String(response.requestUrl) + } else if (options) { + const u = (options as Record).url + if (u) reqUrl = String(u) + else if ((options as Record).href) reqUrl = String((options as Record).href) + else if ((options as Record).pathname) reqUrl = String((options as Record).pathname) } - const resHeaders = response.headers as Record | undefined + const resHeaders = response?.headers as Record | undefined return { request: { method: String(options?.method || 'POST'), - url: String(response.requestUrl || (options?.url?.toString()) || 'unknown'), + url: reqUrl, headers: cleanHeaders, body: options?.json || options?.body || null, }, response: { - statusCode: Number(response.statusCode), + statusCode: Number(response?.statusCode || 0), headers: resHeaders || {}, - body: response.body || null, + body: response?.body || null, + errorCode: code, + errorMessage: message, }, } } @@ -147,56 +224,49 @@ async function processBatch( async function retryOnError( env: TrueApiEnv, batch: string[], token: string, results: SingleCodeResult[], attempt = 0 ): Promise { - try { - const { body: response } = await apiClient(env).post( - config.trueApi.cisesInfoPath, - { - json: { cisList: batch }, - headers: { Authorization: `Bearer ${token}` }, - timeout: { request: 30000 }, - } - ) - - for (const item of response.cisInfo) { - results.push({ - code: item.requestedCis, - found: !!item.cis, - valid: item.status === 'INTRODUCED' || item.status === 'APPLIED' || item.status === 'EMITTED', - status: item.status || 'UNKNOWN', - gtin: item.gtin, - productName: item.productName, - producerName: item.producerName, - ownerName: item.ownerName, - }) - } - } catch (err) { - if (is401(err)) { - const debugInfo = extractDebugInfo(err) - throw new AppError('Token expired', 401, debugInfo) + const response = await apiClient(env).post( + config.trueApi.cisesInfoPath, + { + json: batch, + headers: { Authorization: `Bearer ${token}` }, + timeout: { request: 30000 }, + throwHttpErrors: false, } + ) - const isRetryable = isRetryableError(err) - if (isRetryable && attempt < config.trueApi.maxRetries) { - const delay = Math.pow(2, attempt) * 500 - await sleep(delay) - return retryOnError(env, batch, token, results, attempt + 1) - } - - throw err + if (response.statusCode === 401) { + const debugInfo = extractDebugInfoFromResponse(response) + throw new AppError('Token expired', 401, debugInfo) } -} -function is401(err: unknown): boolean { - if (!err || typeof err !== 'object') return false - return 'response' in err && - (err as { response?: { statusCode?: number } }).response?.statusCode === 401 -} + if (response.statusCode === 429 || (response.statusCode >= 500 && attempt < config.trueApi.maxRetries)) { + const delay = Math.pow(2, attempt) * 500 + await sleep(delay) + return retryOnError(env, batch, token, results, attempt + 1) + } -function isRetryableError(err: unknown): boolean { - if (!err || typeof err !== 'object') return true - const statusCode = (err as { response?: { statusCode?: number } }).response?.statusCode - if (!statusCode) return true - return statusCode === 429 || statusCode >= 500 + if (!response.body || !Array.isArray(response.body)) { + const debugInfo = extractDebugInfoFromResponse(response) + const apiErr = response.body && typeof response.body === 'object' + ? (response.body as Record).error_message || '' + : '' + throw new AppError(`API error: ${apiErr || response.statusCode}`, response.statusCode, debugInfo) + } + + const items = response.body as CisInfoResponseItem[] + for (const item of items) { + const ci = item.cisInfo + results.push({ + code: ci.requestedCis, + found: !!ci.cis && !item.errorCode, + valid: ci.status === 'INTRODUCED' || ci.status === 'APPLIED' || ci.status === 'EMITTED', + status: item.errorCode || ci.status || 'UNKNOWN', + gtin: ci.gtin, + productName: ci.productName, + producerName: ci.producerName, + ownerName: ci.ownerName, + }) + } } function sleep(ms: number): Promise { @@ -213,12 +283,4 @@ export class AppError extends Error { } } -function buildResponse(results: SingleCodeResult[]): CheckCodesResponse { - return { - results, - total: results.length, - validCount: results.filter(r => r.valid).length, - invalidCount: results.filter(r => !r.valid && !r.error).length, - errorCount: results.filter(r => !!r.error).length, - } -} + diff --git a/backend/src/types/index.ts b/backend/src/types/index.ts index 0be152f..57b9786 100644 --- a/backend/src/types/index.ts +++ b/backend/src/types/index.ts @@ -6,6 +6,7 @@ export interface AuthKeyResponse { export interface AuthSignInRequest { uuid: string data: string + inn?: string } export interface AuthSignInResponse { @@ -14,10 +15,6 @@ export interface AuthSignInResponse { expireDate?: string } -export interface CisInfoRequest { - cisList: string[] -} - export interface CisInfoItem { requestedCis: string cis?: string @@ -33,8 +30,10 @@ export interface CisInfoItem { error?: string } -export interface CisInfoResponse { - cisInfo: CisInfoItem[] +export interface CisInfoResponseItem { + cisInfo: CisInfoItem + errorMessage?: string | null + errorCode?: string | null } export interface PublicCheckResponse { @@ -48,6 +47,22 @@ export interface CheckCodesRequest { codes: string[] } +export interface ApiDebugInfo { + request: { + method: string + url: string + headers: Record + body: unknown + } + response: { + statusCode: number + headers: Record + body: unknown + errorCode?: string + errorMessage?: string + } +} + export interface SingleCodeResult { code: string found: boolean @@ -66,4 +81,5 @@ export interface CheckCodesResponse { validCount: number invalidCount: number errorCount: number + debugInfo?: ApiDebugInfo } diff --git a/frontend/src/pages/AuthPage.tsx b/frontend/src/pages/AuthPage.tsx index 758f85e..01dd6e0 100644 --- a/frontend/src/pages/AuthPage.tsx +++ b/frontend/src/pages/AuthPage.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { getAuthStatus, getAuthKey, signIn } from '../services/api' +import { getAuthStatus, getAuthKey, signIn, setTokenManually } from '../services/api' import type { RequestError } from '../services/api' import { isCadesPluginAvailable, listCertificates, signWithCadesPlugin } from '../services/cadesplugin' import type { CertInfo } from '../services/cadesplugin' @@ -13,10 +13,13 @@ export function AuthPage({ onAuth }: Props) { const [hasPlugin, setHasPlugin] = useState(null) const [certs, setCerts] = useState([]) const [selectedThumbprint, setSelectedThumbprint] = useState('') + const [inn, setInn] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [errorDebug, setErrorDebug] = useState(null) const [debugExpanded, setDebugExpanded] = useState(false) + const [manualToken, setManualToken] = useState('') + const [manualLoading, setManualLoading] = useState(false) useEffect(() => { getAuthStatus().then(setStatus).catch(() => {}) @@ -34,6 +37,27 @@ export function AuthPage({ onAuth }: Props) { } } + async function handleSetToken() { + setManualLoading(true) + setError('') + setErrorDebug(null) + setDebugExpanded(false) + try { + await setTokenManually(manualToken) + const s = await getAuthStatus() + setStatus(s) + if (s.authenticated) onAuth() + } catch (err) { + const re = err as RequestError + setError(re.message) + if (re.debugInfo) { + setErrorDebug(JSON.stringify(re.debugInfo, null, 2)) + } + } finally { + setManualLoading(false) + } + } + async function handleSignIn() { setLoading(true) setError('') @@ -42,7 +66,7 @@ export function AuthPage({ onAuth }: Props) { try { const { uuid, data } = await getAuthKey() const signature = await signWithCadesPlugin(data, selectedThumbprint || undefined) - await signIn(uuid, signature) + await signIn(uuid, signature, inn || undefined) const s = await getAuthStatus() setStatus(s) if (s.authenticated) onAuth() @@ -92,6 +116,19 @@ export function AuthPage({ onAuth }: Props) { )}
+
+ + setInn(e.target.value)} + placeholder="Введите ИНН для однозначной авторизации" + style={inputStyle} + /> +
+ {hasPlugin && certs.length > 0 && (