From 8f0f0cd36eb84acd732d92a23c6d2810a64ac274 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 8 Jul 2026 15:10:29 +0000 Subject: [PATCH] ok --- backend/.env.example | 3 +- backend/src/config.ts | 15 +- backend/src/index.ts | 2 + backend/src/routes/auth.ts | 78 +- backend/src/routes/cises.ts | 20 +- backend/src/routes/plans.ts | 41 ++ backend/src/routes/upd.ts | 17 +- backend/src/services/auth-service.ts | 70 +- backend/src/services/cises-service.ts | 98 ++- backend/src/services/token-cache.ts | 27 +- cadesplugin_api.js | 895 ----------------------- docker-compose.yml | 2 + frontend/package.json | 3 +- frontend/src/App.tsx | 60 +- frontend/src/pages/AuthPage.tsx | 46 +- frontend/src/pages/CheckCodesPage.tsx | 46 +- frontend/src/pages/UploadUpdPage.tsx | 46 +- frontend/src/services/api.ts | 99 ++- frontend/src/types/index.ts | 21 + plans/chestny-znak-web-app-2026-07-08.md | 11 + plans/connect-sandbox-2026-07-08.md | 85 +++ plans/service-ukep-2026-07-08.md | 476 ++++++++++++ 22 files changed, 1129 insertions(+), 1032 deletions(-) create mode 100644 backend/src/routes/plans.ts delete mode 100644 cadesplugin_api.js create mode 100644 plans/connect-sandbox-2026-07-08.md create mode 100644 plans/service-ukep-2026-07-08.md diff --git a/backend/.env.example b/backend/.env.example index e9ac3c6..be7d1f5 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,5 +1,6 @@ PORT=3001 HOST=0.0.0.0 -TRUE_API_URL=https://markirovka.sandbox.crptech.ru/api/v3/true-api +SANDBOX_URL=https://markirovka.sandbox.crptech.ru/api/v3/true-api +PROD_URL=https://markirovka.crpt.ru/api/v3/true-api PUBLIC_CHECK_URL=https://mobile.api.crpt.ru/mobile/check CORS_ORIGIN=* diff --git a/backend/src/config.ts b/backend/src/config.ts index a52c73a..4f6bb30 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -1,3 +1,16 @@ +export type TrueApiEnv = 'sandbox' | 'prod' + +export const envUrls: Record = { + sandbox: { + baseUrl: process.env.SANDBOX_URL || 'https://markirovka.sandbox.crptech.ru/api/v3/true-api', + publicCheckUrl: process.env.SANDBOX_PUBLIC_CHECK_URL || 'https://mobile.api.crpt.ru/mobile/check', + }, + prod: { + baseUrl: process.env.PROD_URL || 'https://markirovka.crpt.ru/api/v3/true-api', + publicCheckUrl: process.env.PROD_PUBLIC_CHECK_URL || 'https://mobile.api.crpt.ru/mobile/check', + }, +} + export const config = { port: parseInt(process.env.PORT || '3001', 10), host: process.env.HOST || '0.0.0.0', @@ -5,8 +18,6 @@ export const config = { corsOrigin: process.env.CORS_ORIGIN || '*', trueApi: { - baseUrl: process.env.TRUE_API_URL || 'https://markirovka.sandbox.crptech.ru/api/v3/true-api', - publicCheckUrl: process.env.PUBLIC_CHECK_URL || 'https://mobile.api.crpt.ru/mobile/check', authPath: '/auth', cisesInfoPath: '/cises/info', codesCheckPath: '/codes/check', diff --git a/backend/src/index.ts b/backend/src/index.ts index 4e382b0..ef7eab5 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -5,6 +5,7 @@ import { config } from './config.js' import { authRoutes } from './routes/auth.js' import { cisesRoutes } from './routes/cises.js' import { updRoutes } from './routes/upd.js' +import { plansRoutes } from './routes/plans.js' const app = Fastify({ logger: true }) @@ -20,6 +21,7 @@ await app.register(multipart, { await app.register(authRoutes) await app.register(cisesRoutes) await app.register(updRoutes) +await app.register(plansRoutes) app.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() })) diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 7964674..cd04786 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -1,6 +1,15 @@ import type { FastifyInstance } from 'fastify' -import { getAuthKey, signIn, clearToken, isAuthenticated } from '../services/auth-service.js' -import { getTokenTTL } from '../services/token-cache.js' +import type { TrueApiEnv } from '../config.js' +import { getAuthKey, signIn, clearToken, getAuthStatus } from '../services/auth-service.js' +import { AppError, extractDebugInfo } from '../services/cises-service.js' + +const VALID_ENVS = ['sandbox', 'prod'] + +function parseEnv(query: Record): TrueApiEnv { + const env = query.env + if (env && VALID_ENVS.includes(env)) return env as TrueApiEnv + return 'sandbox' +} const signInSchema = { body: { @@ -14,28 +23,55 @@ const signInSchema = { } export async function authRoutes(app: FastifyInstance): Promise { - app.get('/auth/key', async () => { - const authKey = await getAuthKey() - return { uuid: authKey.uuid, data: authKey.data } - }) - - app.post<{ Body: { uuid: string; data: string } }>('/auth/simpleSignIn', { schema: signInSchema }, async (request) => { - const { uuid, data } = request.body - const result = await signIn({ uuid, data }) - return result - }) - - app.get('/auth/status', async () => { - const authenticated = isAuthenticated() - const ttl = getTokenTTL() - return { - authenticated, - tokenExpiresAt: ttl ? new Date(ttl).toISOString() : null, + app.get('/auth/key', async (request, reply) => { + const env = parseEnv(request.query as Record) + try { + const authKey = await getAuthKey(env) + return { uuid: authKey.uuid, data: authKey.data } + } catch (err) { + if (err instanceof AppError) { + return reply.status(err.statusCode).send({ + error: err.message, + debugInfo: err.debugInfo, + }) + } + const debugInfo = extractDebugInfo(err) + return reply.status(502).send({ + error: 'Failed to get auth key from True API', + debugInfo, + }) } }) - app.post('/auth/logout', async () => { - clearToken() + app.post<{ Body: { uuid: string; data: 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 }) + return result + } catch (err) { + if (err instanceof AppError) { + return reply.status(err.statusCode).send({ + error: err.message, + debugInfo: err.debugInfo, + }) + } + const debugInfo = extractDebugInfo(err) + return reply.status(502).send({ + error: 'Failed to sign in to True API', + debugInfo, + }) + } + }) + + app.get('/auth/status', async (request) => { + const env = parseEnv(request.query as Record) + return getAuthStatus(env) + }) + + app.post('/auth/logout', async (request) => { + const env = parseEnv(request.query as Record) + clearToken(env) return { success: true } }) } diff --git a/backend/src/routes/cises.ts b/backend/src/routes/cises.ts index b3e0e86..c594a4b 100644 --- a/backend/src/routes/cises.ts +++ b/backend/src/routes/cises.ts @@ -1,7 +1,16 @@ import type { FastifyInstance } from 'fastify' +import type { TrueApiEnv } from '../config.js' import { checkCodesPublic, checkCodesAuth, AppError } from '../services/cises-service.js' import type { CheckCodesRequest } from '../types/index.js' +const VALID_ENVS = ['sandbox', 'prod'] + +function parseEnv(query: Record): TrueApiEnv { + const env = query.env + if (env && VALID_ENVS.includes(env)) return env as TrueApiEnv + return 'sandbox' +} + const checkCodesSchema = { body: { type: 'object', @@ -19,15 +28,20 @@ const checkCodesSchema = { export async function cisesRoutes(app: FastifyInstance): Promise { app.post<{ Body: CheckCodesRequest }>('/api/check-codes/public', { schema: checkCodesSchema }, async (request) => { - return checkCodesPublic(request.body) + const env = parseEnv(request.query as Record) + return checkCodesPublic(env, request.body) }) app.post<{ Body: CheckCodesRequest }>('/api/check-codes/auth', { schema: checkCodesSchema }, async (request, reply) => { + const env = parseEnv(request.query as Record) try { - return await checkCodesAuth(request.body) + return await checkCodesAuth(env, request.body) } catch (err) { if (err instanceof AppError) { - return reply.status(err.statusCode).send({ error: err.message }) + return reply.status(err.statusCode).send({ + error: err.message, + debugInfo: err.debugInfo, + }) } throw err } diff --git a/backend/src/routes/plans.ts b/backend/src/routes/plans.ts new file mode 100644 index 0000000..4f87084 --- /dev/null +++ b/backend/src/routes/plans.ts @@ -0,0 +1,41 @@ +import { readdir, readFile, stat } from 'node:fs/promises' +import { resolve, extname } from 'node:path' +import type { FastifyInstance } from 'fastify' + +const PLANS_DIR = resolve(import.meta.dirname || __dirname, '../../plans') + +export async function plansRoutes(app: FastifyInstance): Promise { + app.get('/api/plans', async () => { + const files = await readdir(PLANS_DIR) + const mdFiles: { name: string; path: string; size: number }[] = [] + + for (const file of files) { + if (extname(file) !== '.md') continue + const fullPath = resolve(PLANS_DIR, file) + const stats = await stat(fullPath) + mdFiles.push({ name: file, path: file, size: stats.size }) + } + + mdFiles.sort((a, b) => b.name.localeCompare(a.name)) + return mdFiles + }) + + app.get<{ Params: { filename: string } }>('/api/plans/:filename', async (request, reply) => { + const { filename } = request.params + + if (extname(filename) !== '.md' || filename.includes('..') || filename.includes('/')) { + return reply.status(400).send({ error: 'Invalid filename' }) + } + + const fullPath = resolve(PLANS_DIR, filename) + + try { + await stat(fullPath) + } catch { + return reply.status(404).send({ error: 'File not found' }) + } + + const content = await readFile(fullPath, 'utf-8') + return { name: filename, content } + }) +} diff --git a/backend/src/routes/upd.ts b/backend/src/routes/upd.ts index 2b2e4f0..aef6af3 100644 --- a/backend/src/routes/upd.ts +++ b/backend/src/routes/upd.ts @@ -1,9 +1,19 @@ import type { FastifyInstance } from 'fastify' +import type { TrueApiEnv } from '../config.js' import { parseUpdXml } from '../services/upd-parser.js' import { checkCodesAuth, AppError } from '../services/cises-service.js' +const VALID_ENVS = ['sandbox', 'prod'] + +function parseEnv(query: Record): TrueApiEnv { + const env = query.env + if (env && VALID_ENVS.includes(env)) return env as TrueApiEnv + return 'sandbox' +} + export async function updRoutes(app: FastifyInstance): Promise { app.post('/api/upload-upd', async (request, reply) => { + const env = parseEnv(request.query as Record) const data = await request.file() if (!data) { return reply.status(400).send({ error: 'No file uploaded' }) @@ -25,7 +35,7 @@ export async function updRoutes(app: FastifyInstance): Promise { } try { - const checkResults = await checkCodesAuth({ codes: parsed.codes }) + const checkResults = await checkCodesAuth(env, { codes: parsed.codes }) return { document: { @@ -40,7 +50,10 @@ export async function updRoutes(app: FastifyInstance): Promise { } } catch (err) { if (err instanceof AppError) { - return reply.status(err.statusCode).send({ error: err.message }) + return reply.status(err.statusCode).send({ + error: err.message, + debugInfo: err.debugInfo, + }) } throw err } diff --git a/backend/src/services/auth-service.ts b/backend/src/services/auth-service.ts index 7297cc4..a9ebf86 100644 --- a/backend/src/services/auth-service.ts +++ b/backend/src/services/auth-service.ts @@ -1,34 +1,58 @@ import got from 'got' -import { config } from '../config.js' -import { getToken, setToken, invalidateToken } from './token-cache.js' +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' -const authClient = got.extend({ - prefixUrl: config.trueApi.baseUrl + config.trueApi.authPath, - responseType: 'json', -}) - -export async function getAuthKey(): Promise { - const { body } = await authClient.get('key') - return body -} - -export async function signIn(body: AuthSignInRequest): Promise { - const { body: response } = await authClient.post('simpleSignIn', { - json: body, +function authClient(env: TrueApiEnv) { + return got.extend({ + prefixUrl: envUrls[env].baseUrl + config.trueApi.authPath, + responseType: 'json', }) - setToken(response.token) - return response } -export function getCachedToken(): string | undefined { - return getToken() +export async function getAuthKey(env: TrueApiEnv): Promise { + try { + const { body } = await authClient(env).get('key') + return body + } catch (err) { + const info = extractDebugInfo(err) + const status = info?.response.statusCode || 502 + throw new AppError(info?.response.body ? String(info.response.body) : 'Failed to get auth key', status, info) + } } -export function clearToken(): void { - invalidateToken() +export async function signIn(env: TrueApiEnv, body: AuthSignInRequest): Promise { + try { + const { body: response } = await authClient(env).post('simpleSignIn', { + json: body, + }) + setToken(env, response.token) + return response + } catch (err) { + const info = extractDebugInfo(err) + const status = info?.response.statusCode || 502 + throw new AppError(info?.response.body ? String(info.response.body) : 'Failed to sign in', status, info) + } } -export function isAuthenticated(): boolean { - return !!getToken() +export function getCachedToken(env: TrueApiEnv): string | undefined { + return getToken(env) +} + +export function clearToken(env: TrueApiEnv): void { + invalidateToken(env) +} + +export function isAuthenticated(env: TrueApiEnv): boolean { + return hasToken(env) +} + +export function getAuthStatus(env: TrueApiEnv): { authenticated: boolean; tokenExpiresAt: string | null } { + const ttl = getTokenTTL(env) + return { + authenticated: hasToken(env), + tokenExpiresAt: ttl ? new Date(ttl).toISOString() : null, + } } diff --git a/backend/src/services/cises-service.ts b/backend/src/services/cises-service.ts index 076b503..780ef84 100644 --- a/backend/src/services/cises-service.ts +++ b/backend/src/services/cises-service.ts @@ -1,5 +1,6 @@ import got from 'got' -import { config } from '../config.js' +import { config, envUrls } from '../config.js' +import type { TrueApiEnv } from '../config.js' import { getCachedToken, clearToken } from './auth-service.js' import type { CisInfoResponse, @@ -9,12 +10,14 @@ import type { SingleCodeResult, } from '../types/index.js' -const apiClient = got.extend({ - prefixUrl: config.trueApi.baseUrl, - responseType: 'json', -}) +function apiClient(env: TrueApiEnv) { + return got.extend({ + prefixUrl: envUrls[env].baseUrl, + responseType: 'json', + }) +} -export async function checkCodesPublic(body: CheckCodesRequest): Promise { +export async function checkCodesPublic(env: TrueApiEnv, body: CheckCodesRequest): Promise { const results: SingleCodeResult[] = [] const queue = [...body.codes] @@ -23,7 +26,7 @@ export async function checkCodesPublic(body: CheckCodesRequest): Promise( - `${config.trueApi.publicCheckUrl}?code=${encodeURIComponent(code)}`, + `${envUrls[env].publicCheckUrl}?code=${encodeURIComponent(code)}`, { responseType: 'json', timeout: { request: 10000 } } ) results.push({ @@ -50,21 +53,21 @@ export async function checkCodesPublic(body: CheckCodesRequest): Promise { - const token = getToken() +export async function checkCodesAuth(env: TrueApiEnv, body: CheckCodesRequest): Promise { + const token = getToken(env) const results: SingleCodeResult[] = [] try { - await processBatch(body.codes, token, results, 0) + await processBatch(env, body.codes, token, results, 0) } catch (err) { if (err instanceof AppError && err.statusCode === 401) { - clearToken() + clearToken(env) throw err } for (const code of body.codes) { @@ -77,8 +80,58 @@ export async function checkCodesAuth(body: CheckCodesRequest): Promise + body: unknown + } + response: { + statusCode: number + headers: Record + body: unknown + } +} + +export function extractDebugInfo(err: unknown): ApiDebugInfo | undefined { + if (!err || typeof err !== 'object') return 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 resHeaders = response.headers as Record | undefined + + return { + request: { + method: String(options?.method || 'POST'), + url: String(response.requestUrl || (options?.url?.toString()) || 'unknown'), + headers: cleanHeaders, + body: options?.json || options?.body || null, + }, + response: { + statusCode: Number(response.statusCode), + headers: resHeaders || {}, + body: response.body || null, + }, + } +} + async function processBatch( - codes: string[], token: string, results: SingleCodeResult[], batchIndex: number + env: TrueApiEnv, codes: string[], token: string, results: SingleCodeResult[], batchIndex: number ): Promise { if (batchIndex >= codes.length) return @@ -86,16 +139,16 @@ async function processBatch( const end = Math.min(start + config.trueApi.batchSize, codes.length) const batch = codes.slice(start, end) - await retryOnError(batch, token, results) + await retryOnError(env, batch, token, results) - await processBatch(codes, token, results, end) + await processBatch(env, codes, token, results, end) } async function retryOnError( - batch: string[], token: string, results: SingleCodeResult[], attempt = 0 + env: TrueApiEnv, batch: string[], token: string, results: SingleCodeResult[], attempt = 0 ): Promise { try { - const { body: response } = await apiClient.post( + const { body: response } = await apiClient(env).post( config.trueApi.cisesInfoPath, { json: { cisList: batch }, @@ -118,14 +171,15 @@ async function retryOnError( } } catch (err) { if (is401(err)) { - throw new AppError('Token expired', 401) + const debugInfo = extractDebugInfo(err) + throw new AppError('Token expired', 401, debugInfo) } const isRetryable = isRetryableError(err) if (isRetryable && attempt < config.trueApi.maxRetries) { const delay = Math.pow(2, attempt) * 500 await sleep(delay) - return retryOnError(batch, token, results, attempt + 1) + return retryOnError(env, batch, token, results, attempt + 1) } throw err @@ -151,9 +205,11 @@ function sleep(ms: number): Promise { export class AppError extends Error { statusCode: number - constructor(message: string, statusCode: number) { + debugInfo?: ApiDebugInfo + constructor(message: string, statusCode: number, debugInfo?: ApiDebugInfo) { super(message) this.statusCode = statusCode + this.debugInfo = debugInfo } } diff --git a/backend/src/services/token-cache.ts b/backend/src/services/token-cache.ts index 5f03244..9119c81 100644 --- a/backend/src/services/token-cache.ts +++ b/backend/src/services/token-cache.ts @@ -1,29 +1,32 @@ import NodeCache from 'node-cache' import { config } from '../config.js' +import type { TrueApiEnv } from '../config.js' const cache = new NodeCache({ stdTTL: config.trueApi.tokenTTL / 1000, checkperiod: 600, }) -const TOKEN_KEY = 'true_api_token' - -export function getToken(): string | undefined { - return cache.get(TOKEN_KEY) +function tokenKey(env: TrueApiEnv): string { + return `true_api_token_${env}` } -export function setToken(token: string): void { - cache.set(TOKEN_KEY, token) +export function getToken(env: TrueApiEnv): string | undefined { + return cache.get(tokenKey(env)) } -export function hasToken(): boolean { - return cache.has(TOKEN_KEY) +export function setToken(env: TrueApiEnv, token: string): void { + cache.set(tokenKey(env), token) } -export function getTokenTTL(): number | undefined { - return cache.getTtl(TOKEN_KEY) +export function hasToken(env: TrueApiEnv): boolean { + return cache.has(tokenKey(env)) } -export function invalidateToken(): void { - cache.del(TOKEN_KEY) +export function getTokenTTL(env: TrueApiEnv): number | undefined { + return cache.getTtl(tokenKey(env)) +} + +export function invalidateToken(env: TrueApiEnv): void { + cache.del(tokenKey(env)) } diff --git a/cadesplugin_api.js b/cadesplugin_api.js deleted file mode 100644 index cd5b5ea..0000000 --- a/cadesplugin_api.js +++ /dev/null @@ -1,895 +0,0 @@ -; (function () { - //already loaded - if (window.cadesplugin && window.cadesplugin.LOG_LEVEL_DEBUG) { - return; - } - var pluginObject; - var plugin_resolved = 0; - var plugin_reject; - var plugin_resolve; - var isOpera = 0; - var isFireFox = 0; - var isSafari = 0; - var isYandex = 0; - var canPromise = !!window.Promise; - var cadesplugin_loaded_event_recieved = false; - var isFireFoxExtensionLoaded = false; - var cadesplugin = {}; - var untrusted_sites_message_shown = false; - - if (canPromise) { - cadesplugin = new window.Promise(function (resolve, reject) { - plugin_resolve = resolve; - plugin_reject = reject; - }); - } - - function check_browser() { - var ua = window.navigator.userAgent, - tem, - M = ua.match(/(opera|yabrowser|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; - if (/trident/i.test(M[1])) { - tem = /\brv[ :]+(\d+)/g.exec(ua) || []; - return { name: 'IE', version: (tem[1] || '') }; - } - if (M[1] === "Chrome") { - tem = ua.match(/\b(OPR|Edg|YaBrowser)\/(\d+)/); - if (tem != null && (tem.length > 2)) { - return { name: tem[1].replace('OPR', 'Opera'), version: tem[2] }; - } - } - M = M[2] ? [M[1], M[2]] : [window.navigator.appName, window.navigator.appVersion, '-?']; - if ((tem = ua.match(/version\/(\d+)/i)) != null) { - M.splice(1, 1, tem[1]); - } - return { name: M[0], version: M[1] }; - } - - var browserSpecs = check_browser(); - - function cpcsp_console_log(level, msg) { - //IE9 не может писать в консоль если не открыта вкладка developer tools - if (typeof console === 'undefined') { - return; - } - if (level <= cadesplugin.current_log_level) { - if (level === cadesplugin.LOG_LEVEL_DEBUG) { - console.log("DEBUG: %s", msg); - } - if (level === cadesplugin.LOG_LEVEL_INFO) { - console.info("INFO: %s", msg); - } - if (level === cadesplugin.LOG_LEVEL_ERROR) { - console.error("ERROR: %s", msg); - } - } - } - - function get_extension_version(callback) { - window.postMessage("cadesplugin_extension_version_request", "*"); - window.addEventListener("message", function (event) { - var resp_prefix = "cadesplugin_extension_version_response:"; - if (typeof (event.data) !== "string" || event.data.indexOf(resp_prefix) !== 0) { - return; - } - var ext_version = event.data.substring(resp_prefix.length); - callback(ext_version); - }, false); - } - - function get_extension_id(callback) { - window.postMessage("cadesplugin_extension_id_request", "*"); - window.addEventListener("message", function (event) { - var resp_prefix = "cadesplugin_extension_id_response:"; - if (typeof (event.data) !== "string" || event.data.indexOf(resp_prefix) !== 0) { - return; - } - var ext_id = event.data.substring(resp_prefix.length); - callback(ext_id); - }, false); - } - - function set_log_level(level) { - if (!((level === cadesplugin.LOG_LEVEL_DEBUG) || - (level === cadesplugin.LOG_LEVEL_INFO) || - (level === cadesplugin.LOG_LEVEL_ERROR))) { - cpcsp_console_log(cadesplugin.LOG_LEVEL_ERROR, "cadesplugin_api.js: Incorrect log_level: " + level); - return; - } - cadesplugin.current_log_level = level; - if (cadesplugin.current_log_level === cadesplugin.LOG_LEVEL_DEBUG) { - cpcsp_console_log(cadesplugin.LOG_LEVEL_INFO, "cadesplugin_api.js: log_level = DEBUG"); - } - if (cadesplugin.current_log_level === cadesplugin.LOG_LEVEL_INFO) { - cpcsp_console_log(cadesplugin.LOG_LEVEL_INFO, "cadesplugin_api.js: log_level = INFO"); - } - if (cadesplugin.current_log_level === cadesplugin.LOG_LEVEL_ERROR) { - cpcsp_console_log(cadesplugin.LOG_LEVEL_INFO, "cadesplugin_api.js: log_level = ERROR"); - } - if (isNativeMessageSupported()) { - if (cadesplugin.current_log_level === cadesplugin.LOG_LEVEL_DEBUG) { - window.postMessage("set_log_level=debug", "*"); - } - if (cadesplugin.current_log_level === cadesplugin.LOG_LEVEL_INFO) { - window.postMessage("set_log_level=info", "*"); - } - if (cadesplugin.current_log_level === cadesplugin.LOG_LEVEL_ERROR) { - window.postMessage("set_log_level=error", "*"); - } - } - } - - function set_constantValues() { - cadesplugin.CAPICOM_MEMORY_STORE = 0; - cadesplugin.CAPICOM_LOCAL_MACHINE_STORE = 1; - cadesplugin.CAPICOM_CURRENT_USER_STORE = 2; - cadesplugin.CAPICOM_SMART_CARD_USER_STORE = 4; - cadesplugin.CADESCOM_MEMORY_STORE = 0; - cadesplugin.CADESCOM_LOCAL_MACHINE_STORE = 1; - cadesplugin.CADESCOM_CURRENT_USER_STORE = 2; - cadesplugin.CADESCOM_SMART_CARD_USER_STORE = 4; - cadesplugin.CADESCOM_CONTAINER_STORE = 100; - - cadesplugin.CAPICOM_ROOT_STORE = "Root"; - cadesplugin.CAPICOM_CA_STORE = "CA"; - cadesplugin.CAPICOM_MY_STORE = "My"; - cadesplugin.CAPICOM_ADDRESSBOOK_STORE = "AddressBook"; - - cadesplugin.CAPICOM_STORE_OPEN_READ_WRITE = 1; - cadesplugin.CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED = 2; - cadesplugin.CAPICOM_STORE_OPEN_INCLUDE_ARCHIVED = 256; - - cadesplugin.CAPICOM_CERTIFICATE_FIND_SUBJECT_NAME = 1; - - cadesplugin.CADESCOM_XML_SIGNATURE_TYPE_ENVELOPED = 0; - cadesplugin.CADESCOM_XML_SIGNATURE_TYPE_ENVELOPING = 1; - cadesplugin.CADESCOM_XML_SIGNATURE_TYPE_TEMPLATE = 2; - - cadesplugin.CADESCOM_XADES_DEFAULT = 0x00000010; - cadesplugin.CADESCOM_XADES_BES = 0x00000020; - cadesplugin.CADESCOM_XADES_T = 0x00000050; - cadesplugin.CADESCOM_XADES_X_LONG_TYPE_1 = 0x000005d0; - cadesplugin.CADESCOM_XMLDSIG_TYPE = 0x00000000; - - cadesplugin.XmlDsigGost3410UrlObsolete = "http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411"; - cadesplugin.XmlDsigGost3411UrlObsolete = "http://www.w3.org/2001/04/xmldsig-more#gostr3411"; - cadesplugin.XmlDsigGost3410Url = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102001-gostr3411"; - cadesplugin.XmlDsigGost3411Url = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr3411"; - - cadesplugin.XmlDsigGost3411Url2012256 = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34112012-256"; - cadesplugin.XmlDsigGost3410Url2012256 = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102012-gostr34112012-256"; - cadesplugin.XmlDsigGost3411Url2012512 = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34112012-512"; - cadesplugin.XmlDsigGost3410Url2012512 = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102012-gostr34112012-512"; - - cadesplugin.CADESCOM_CADES_DEFAULT = 0; - cadesplugin.CADESCOM_CADES_BES = 1; - cadesplugin.CADESCOM_CADES_T = 0x5; - cadesplugin.CADESCOM_CADES_X_LONG_TYPE_1 = 0x5d; - cadesplugin.CADESCOM_CADES_A = 0xdd; - cadesplugin.CADESCOM_PKCS7_TYPE = 0xffff; - - cadesplugin.CADESCOM_ENCODE_BASE64 = 0; - cadesplugin.CADESCOM_ENCODE_BINARY = 1; - cadesplugin.CADESCOM_ENCODE_ANY = -1; - - cadesplugin.CAPICOM_CERTIFICATE_INCLUDE_CHAIN_EXCEPT_ROOT = 0; - cadesplugin.CAPICOM_CERTIFICATE_INCLUDE_WHOLE_CHAIN = 1; - cadesplugin.CAPICOM_CERTIFICATE_INCLUDE_END_ENTITY_ONLY = 2; - cadesplugin.CAPICOM_CERTIFICATE_INCLUDE_NONE = 0x100; - - cadesplugin.CAPICOM_CERT_INFO_SUBJECT_SIMPLE_NAME = 0; - cadesplugin.CAPICOM_CERT_INFO_ISSUER_SIMPLE_NAME = 1; - - cadesplugin.CAPICOM_CERTIFICATE_FIND_SHA1_HASH = 0; - cadesplugin.CAPICOM_CERTIFICATE_FIND_SUBJECT_NAME = 1; - cadesplugin.CAPICOM_CERTIFICATE_FIND_ISSUER_NAME = 2; - cadesplugin.CAPICOM_CERTIFICATE_FIND_ROOT_NAME = 3; - cadesplugin.CAPICOM_CERTIFICATE_FIND_TEMPLATE_NAME = 4; - cadesplugin.CAPICOM_CERTIFICATE_FIND_EXTENSION = 5; - cadesplugin.CAPICOM_CERTIFICATE_FIND_EXTENDED_PROPERTY = 6; - cadesplugin.CAPICOM_CERTIFICATE_FIND_APPLICATION_POLICY = 7; - cadesplugin.CAPICOM_CERTIFICATE_FIND_CERTIFICATE_POLICY = 8; - cadesplugin.CAPICOM_CERTIFICATE_FIND_TIME_VALID = 9; - cadesplugin.CAPICOM_CERTIFICATE_FIND_TIME_NOT_YET_VALID = 10; - cadesplugin.CAPICOM_CERTIFICATE_FIND_TIME_EXPIRED = 11; - cadesplugin.CAPICOM_CERTIFICATE_FIND_KEY_USAGE = 12; - - cadesplugin.CAPICOM_DIGITAL_SIGNATURE_KEY_USAGE = 128; - - cadesplugin.CAPICOM_PROPID_ENHKEY_USAGE = 9; - - cadesplugin.CAPICOM_OID_OTHER = 0; - cadesplugin.CAPICOM_OID_KEY_USAGE_EXTENSION = 10; - - cadesplugin.CAPICOM_EKU_CLIENT_AUTH = 2; - cadesplugin.CAPICOM_EKU_SMARTCARD_LOGON = 5; - cadesplugin.CAPICOM_EKU_OTHER = 0; - - cadesplugin.CAPICOM_AUTHENTICATED_ATTRIBUTE_SIGNING_TIME = 0; - cadesplugin.CAPICOM_AUTHENTICATED_ATTRIBUTE_DOCUMENT_NAME = 1; - cadesplugin.CAPICOM_AUTHENTICATED_ATTRIBUTE_DOCUMENT_DESCRIPTION = 2; - cadesplugin.CADESCOM_AUTHENTICATED_ATTRIBUTE_SIGNING_TIME = 0; - cadesplugin.CADESCOM_AUTHENTICATED_ATTRIBUTE_DOCUMENT_NAME = 1; - cadesplugin.CADESCOM_AUTHENTICATED_ATTRIBUTE_DOCUMENT_DESCRIPTION = 2; - cadesplugin.CADESCOM_AUTHENTICATED_ATTRIBUTE_MACHINE_INFO = 0x100; - cadesplugin.CADESCOM_ATTRIBUTE_OTHER = -1; - - cadesplugin.CADESCOM_STRING_TO_UCS2LE = 0; - cadesplugin.CADESCOM_BASE64_TO_BINARY = 1; - - cadesplugin.CADESCOM_DISPLAY_DATA_NONE = 0; - cadesplugin.CADESCOM_DISPLAY_DATA_CONTENT = 1; - cadesplugin.CADESCOM_DISPLAY_DATA_ATTRIBUTE = 2; - - cadesplugin.CADESCOM_ENCRYPTION_ALGORITHM_RC2 = 0; - cadesplugin.CADESCOM_ENCRYPTION_ALGORITHM_RC4 = 1; - cadesplugin.CADESCOM_ENCRYPTION_ALGORITHM_DES = 2; - cadesplugin.CADESCOM_ENCRYPTION_ALGORITHM_3DES = 3; - cadesplugin.CADESCOM_ENCRYPTION_ALGORITHM_AES = 4; - cadesplugin.CADESCOM_ENCRYPTION_ALGORITHM_GOST_28147_89 = 25; - cadesplugin.CADESCOM_ENCRYPTION_ALGORITHM_GOST_MAGMA = 35; - cadesplugin.CADESCOM_ENCRYPTION_ALGORITHM_GOST_MAGMA_OMAC = 36; - cadesplugin.CADESCOM_ENCRYPTION_ALGORITHM_GOST_KUZNYECHIK = 45; - cadesplugin.CADESCOM_ENCRYPTION_ALGORITHM_GOST_KUZNYECHIK_OMAC = 46; - - cadesplugin.CADESCOM_HASH_ALGORITHM_SHA1 = 0; - cadesplugin.CADESCOM_HASH_ALGORITHM_MD2 = 1; - cadesplugin.CADESCOM_HASH_ALGORITHM_MD4 = 2; - cadesplugin.CADESCOM_HASH_ALGORITHM_MD5 = 3; - cadesplugin.CADESCOM_HASH_ALGORITHM_SHA_256 = 4; - cadesplugin.CADESCOM_HASH_ALGORITHM_SHA_384 = 5; - cadesplugin.CADESCOM_HASH_ALGORITHM_SHA_512 = 6; - cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411 = 100; - cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2012_256 = 101; - cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2012_512 = 102; - cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411_HMAC = 110; - cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2012_256_HMAC = 111; - cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2012_512_HMAC = 112; - - cadesplugin.CADESCOM_CERT_INFO_ROLE = 100; - cadesplugin.CADESCOM_ROLE_ROOT = "ROOT"; - cadesplugin.CADESCOM_ROLE_CA = "CA"; - cadesplugin.CADESCOM_ROLE_LEAF = "LEAF"; - - cadesplugin.LOG_LEVEL_DEBUG = 4; - cadesplugin.LOG_LEVEL_INFO = 2; - cadesplugin.LOG_LEVEL_ERROR = 1; - - cadesplugin.CADESCOM_AllowNone = 0; - cadesplugin.CADESCOM_AllowNoOutstandingRequest = 0x1; - cadesplugin.CADESCOM_AllowUntrustedCertificate = 0x2; - cadesplugin.CADESCOM_AllowUntrustedRoot = 0x4; - cadesplugin.CADESCOM_SkipInstallToStore = 0x10000000; - cadesplugin.CADESCOM_InstallCertChainToContainer = 0x20000000; - cadesplugin.CADESCOM_UseContainerStore = 0x40000000; - - cadesplugin.ContextNone = 0; - cadesplugin.ContextUser = 0x1; - cadesplugin.ContextMachine = 0x2; - cadesplugin.ContextAdministratorForceMachine = 0x3; - - cadesplugin.ENABLE_CARRIER_TYPE_CSP = 0x01; - cadesplugin.ENABLE_CARRIER_TYPE_FKC_NO_SM = 0x02; - cadesplugin.ENABLE_CARRIER_TYPE_FKC_SM = 0x04; - cadesplugin.ENABLE_ANY_CARRIER_TYPE = 0x07; - - cadesplugin.DISABLE_EVERY_CARRIER_OPERATION = 0x00; - cadesplugin.ENABLE_CARRIER_OPEN_ENUM = 0x01; - cadesplugin.ENABLE_CARRIER_CREATE = 0x02; - cadesplugin.ENABLE_ANY_OPERATION = 0x03; - - cadesplugin.CADESCOM_PRODUCT_CSP = 0; - cadesplugin.CADESCOM_PRODUCT_OCSP = 1; - cadesplugin.CADESCOM_PRODUCT_TSP = 2; - - cadesplugin.MEDIA_TYPE_DEFAULT = 0x00000000; - cadesplugin.MEDIA_TYPE_REGISTRY = 0x00000001; - cadesplugin.MEDIA_TYPE_HDIMAGE = 0x00000002; - cadesplugin.MEDIA_TYPE_CLOUD = 0x00000004; - cadesplugin.MEDIA_TYPE_SCARD = 0x00000008; - - cadesplugin.XCN_CRYPT_STRING_BASE64HEADER = 0; - cadesplugin.XCN_CRYPT_STRING_BASE64 = 0x1; - cadesplugin.XCN_CRYPT_STRING_BINARY = 0x2; - cadesplugin.XCN_CRYPT_STRING_BASE64REQUESTHEADER = 0x3; - cadesplugin.XCN_CRYPT_STRING_HEX = 0x4; - cadesplugin.XCN_CRYPT_STRING_HEXASCII = 0x5; - cadesplugin.XCN_CRYPT_STRING_BASE64_ANY = 0x6; - cadesplugin.XCN_CRYPT_STRING_ANY = 0x7; - cadesplugin.XCN_CRYPT_STRING_HEX_ANY = 0x8; - cadesplugin.XCN_CRYPT_STRING_BASE64X509CRLHEADER = 0x9; - cadesplugin.XCN_CRYPT_STRING_HEXADDR = 0xa; - cadesplugin.XCN_CRYPT_STRING_HEXASCIIADDR = 0xb; - cadesplugin.XCN_CRYPT_STRING_HEXRAW = 0xc; - cadesplugin.XCN_CRYPT_STRING_BASE64URI = 0xd; - cadesplugin.XCN_CRYPT_STRING_ENCODEMASK = 0xff; - cadesplugin.XCN_CRYPT_STRING_CHAIN = 0x100; - cadesplugin.XCN_CRYPT_STRING_TEXT = 0x200; - cadesplugin.XCN_CRYPT_STRING_PERCENTESCAPE = 0x8000000; - cadesplugin.XCN_CRYPT_STRING_HASHDATA = 0x10000000; - cadesplugin.XCN_CRYPT_STRING_STRICT = 0x20000000; - cadesplugin.XCN_CRYPT_STRING_NOCRLF = 0x40000000; - cadesplugin.XCN_CRYPT_STRING_NOCR = 0x80000000; - - cadesplugin.XCN_CERT_NAME_STR_NONE = 0; - cadesplugin.XCN_AT_NONE = 0; - cadesplugin.XCN_AT_KEYEXCHANGE = 1; - cadesplugin.XCN_AT_SIGNATURE = 2; - - cadesplugin.AT_KEYEXCHANGE = 1; - cadesplugin.AT_SIGNATURE = 2; - - cadesplugin.CARRIER_FLAG_REMOVABLE = 1; - cadesplugin.CARRIER_FLAG_UNIQUE = 2; - cadesplugin.CARRIER_FLAG_PROTECTED = 4; - cadesplugin.CARRIER_FLAG_FUNCTIONAL_CARRIER = 8; - cadesplugin.CARRIER_FLAG_SECURE_MESSAGING = 16; - cadesplugin.CARRIER_FLAG_ABLE_SET_KEY = 32; - cadesplugin.CARRIER_FLAG_ABLE_VISUALISE_SIGNATURE = 64; - cadesplugin.CARRIER_FLAG_VIRTUAL = 128; - - cadesplugin.CRYPT_MODE_CBCSTRICT = 1; - cadesplugin.CRYPT_MODE_CNT = 3; - cadesplugin.CRYPT_MODE_CBCRFC4357 = 31; - cadesplugin.CRYPT_MODE_CTR = 32; - cadesplugin.CRYPT_MODE_MGM = 33; - cadesplugin.CRYPT_MODE_GCM = 34; - cadesplugin.CRYPT_MODE_OMAC_CTR = 35; - cadesplugin.CRYPT_MODE_WRAP = 36; - cadesplugin.CRYPT_MODE_WRAP_PAD = 37; - - cadesplugin.PKCS5_PADDING = 1; - cadesplugin.RANDOM_PADDING = 2; - cadesplugin.ZERO_PADDING = 3; - cadesplugin.ISO10126_PADDING = 4; - cadesplugin.ANSI_X923_PADDING = 5; - cadesplugin.TLS_1_0_PADDING = 6; - cadesplugin.ISO_IEC_7816_4_PADDING = 7; - - cadesplugin.CAPICOM_STORE_SAVE_AS_SERIALIZED = 0; - cadesplugin.CAPICOM_STORE_SAVE_AS_PKCS7 = 1; - - cadesplugin.CADESCOM_IDSCANNER_IMAGE_TYPE_W = 0x00; - cadesplugin.CADESCOM_IDSCANNER_IMAGE_TYPE_IR = 0x01; - cadesplugin.CADESCOM_IDSCANNER_IMAGE_TYPE_UV = 0x02; - - cadesplugin.CERT_TRUST_NO_ERROR = 0x00000000; - cadesplugin.CERT_TRUST_IS_NOT_TIME_VALID = 0x00000001; - cadesplugin.CERT_TRUST_IS_REVOKED = 0x00000004; - cadesplugin.CERT_TRUST_IS_NOT_SIGNATURE_VALID = 0x00000008; - cadesplugin.CERT_TRUST_IS_NOT_VALID_FOR_USAGE = 0x00000010; - cadesplugin.CERT_TRUST_IS_UNTRUSTED_ROOT = 0x00000020; - cadesplugin.CERT_TRUST_REVOCATION_STATUS_UNKNOWN = 0x00000040; - cadesplugin.CERT_TRUST_IS_CYCLIC = 0x00000080; - cadesplugin.CERT_TRUST_INVALID_EXTENSION = 0x00000100; - cadesplugin.CERT_TRUST_INVALID_POLICY_CONSTRAINTS = 0x00000200; - cadesplugin.CERT_TRUST_INVALID_BASIC_CONSTRAINTS = 0x00000400; - cadesplugin.CERT_TRUST_INVALID_NAME_CONSTRAINTS = 0x00000800; - cadesplugin.CERT_TRUST_HAS_NOT_SUPPORTED_NAME_CONSTRAINT = 0x00001000; - cadesplugin.CERT_TRUST_HAS_NOT_DEFINED_NAME_CONSTRAINT = 0x00002000; - cadesplugin.CERT_TRUST_HAS_NOT_PERMITTED_NAME_CONSTRAINT = 0x00004000; - cadesplugin.CERT_TRUST_HAS_EXCLUDED_NAME_CONSTRAINT = 0x00008000; - cadesplugin.CERT_TRUST_IS_OFFLINE_REVOCATION = 0x01000000; - cadesplugin.CERT_TRUST_NO_ISSUANCE_CHAIN_POLICY = 0x02000000; - cadesplugin.CERT_TRUST_IS_EXPLICIT_DISTRUST = 0x04000000; - cadesplugin.CERT_TRUST_HAS_NOT_SUPPORTED_CRITICAL_EXT = 0x08000000; - cadesplugin.CERT_TRUST_HAS_WEAK_SIGNATURE = 0x00100000; - - cadesplugin.XCN_CERT_NO_KEY_USAGE = 0; - cadesplugin.XCN_CERT_DIGITAL_SIGNATURE_KEY_USAGE = 0x80; - cadesplugin.XCN_CERT_NON_REPUDIATION_KEY_USAGE = 0x40; - cadesplugin.XCN_CERT_KEY_ENCIPHERMENT_KEY_USAGE = 0x20; - cadesplugin.XCN_CERT_DATA_ENCIPHERMENT_KEY_USAGE = 0x10; - cadesplugin.XCN_CERT_KEY_AGREEMENT_KEY_USAGE = 0x8; - cadesplugin.XCN_CERT_KEY_CERT_SIGN_KEY_USAGE = 0x4; - cadesplugin.XCN_CERT_OFFLINE_CRL_SIGN_KEY_USAGE = 0x2; - cadesplugin.XCN_CERT_CRL_SIGN_KEY_USAGE = 0x2; - cadesplugin.XCN_CERT_ENCIPHER_ONLY_KEY_USAGE = 0x1; - cadesplugin.XCN_CERT_DECIPHER_ONLY_KEY_USAGE = 0x8000; - - cadesplugin.CADESCOM_XADES_ACCEPT_ANY_ID_ATTR_NAMESPACE = 1; - cadesplugin.CADES_USE_OCSP_AUTHORIZED_POLICY = 0x00020000; - - cadesplugin.XCN_NCRYPT_NO_OPERATION = 0; - cadesplugin.XCN_NCRYPT_CIPHER_OPERATION = 0x1; - cadesplugin.XCN_NCRYPT_HASH_OPERATION = 0x2; - cadesplugin.XCN_NCRYPT_ASYMMETRIC_ENCRYPTION_OPERATION = 0x4; - cadesplugin.XCN_NCRYPT_SECRET_AGREEMENT_OPERATION = 0x8; - cadesplugin.XCN_NCRYPT_SIGNATURE_OPERATION = 0x10; - cadesplugin.XCN_NCRYPT_RNG_OPERATION = 0x20; - - cadesplugin.XCN_CRYPT_ANY_GROUP_ID = 0; - cadesplugin.XCN_CRYPT_HASH_ALG_OID_GROUP_ID = 1; - cadesplugin.XCN_CRYPT_ENCRYPT_ALG_OID_GROUP_ID = 2; - cadesplugin.XCN_CRYPT_PUBKEY_ALG_OID_GROUP_ID = 3; - cadesplugin.XCN_CRYPT_SIGN_ALG_OID_GROUP_ID = 4; - cadesplugin.XCN_CRYPT_RDN_ATTR_OID_GROUP_ID = 5; - cadesplugin.XCN_CRYPT_EXT_OR_ATTR_OID_GROUP_ID = 6; - cadesplugin.XCN_CRYPT_ENHKEY_USAGE_OID_GROUP_ID = 7; - cadesplugin.XCN_CRYPT_POLICY_OID_GROUP_ID = 8; - cadesplugin.XCN_CRYPT_TEMPLATE_OID_GROUP_ID = 9; - - cadesplugin.XCN_CRYPT_OID_INFO_PUBKEY_ANY = 0; - cadesplugin.XCN_CRYPT_OID_INFO_PUBKEY_SIGN_KEY_FLAG = 0x80000000; - cadesplugin.XCN_CRYPT_OID_INFO_PUBKEY_ENCRYPT_KEY_FLAG = 0x40000000; - - cadesplugin.CONTROL_KEY_TIME_VALIDITY_DISABLED = 0; - cadesplugin.CONTROL_KEY_TIME_VALIDITY_ENABLED = 1; - cadesplugin.CONTROL_KEY_TIME_VALIDITY_STRICT = 2; - - cadesplugin.AlgorithmFlagsNone = 0; - cadesplugin.AlgorithmFlagsWrap = 0x1; - } - - function async_spawn(generatorFunc) { - function continuer(verb, arg) { - var result; - try { - result = generator[verb](arg); - } catch (err) { - return window.Promise.reject(err); - } - if (result.done) { - return result.value; - } else { - return window.Promise.resolve(result.value).then(onFulfilled, onRejected); - } - } - var generator = generatorFunc(Array.prototype.slice.call(arguments, 1)); - var onFulfilled = continuer.bind(continuer, "next"); - var onRejected = continuer.bind(continuer, "throw"); - return onFulfilled(); - } - - function isIE() { - return (browserSpecs.name === 'IE' || browserSpecs.name === 'MSIE'); - } - - function isIOS() { - return (window.navigator.userAgent.match(/ipod/i) || - window.navigator.userAgent.match(/ipad/i) || - window.navigator.userAgent.match(/iphone/i)); - } - - function isNativeMessageSupported() { - if (isIE()) { - return false; - } - if (browserSpecs.name === 'Edg') { - return true; - } - if (browserSpecs.name === 'YaBrowser') { - isYandex = true; - return true; - } - if (browserSpecs.name === 'Opera') { - isOpera = true; - return (browserSpecs.version >= 33); - } - if (browserSpecs.name === 'Firefox') { - isFireFox = true; - return (browserSpecs.version >= 52); - } - if (browserSpecs.name === 'Chrome') { - return (browserSpecs.version >= 42); - } - if (browserSpecs.name === 'Safari') { - isSafari = true; - return (browserSpecs.version >= 12); - } - } - - function untrustedSitesHandle(msg) { - if (window.cadesplugin_untrusted_sites_disabled_callback) { - window.cadesplugin_untrusted_sites_disabled_callback(msg); - } else { - if (!untrusted_sites_message_shown) { - untrusted_sites_message_shown = true; - alert("Данный сайт отсутствует в Списке доверенных узлов. Работа плагина плагина на таких сайтах запрещена. Обратитесь к Администратору."); - } - } - } - - function CreateObject(name) { - if (isIOS()) { - return call_ru_cryptopro_npcades_10_native_bridge("CreateObject", [name]); - } - var objWebClassFactory; - if (isIE()) { - if (name.match(/X509Enrollment/i)) { - try { - objWebClassFactory = document.getElementById("webClassFactory"); - return objWebClassFactory.CreateObject(name); - } - catch (e) { - var err = cadesplugin.getLastError(e); - if (err.indexOf("0xC2110F00") !== -1) { - var msg = "Данный сайт отсутствует в Списке доверенных узлов. (0xC2110F00)"; - untrustedSitesHandle(msg); - throw (msg); - } - try { - var objCertEnrollClassFactory = document.getElementById("certEnrollClassFactory"); - return objCertEnrollClassFactory.CreateObject(name); - } - catch (err) { - throw ("Для создания обьектов X509Enrollment следует настроить веб-узел на использование проверки подлинности по протоколу HTTPS"); - } - } - } - try { - objWebClassFactory = document.getElementById("webClassFactory"); - return objWebClassFactory.CreateObject(name); - } catch (e) { - var err = cadesplugin.getLastError(e); - if (err.indexOf("0xC2110F00") !== -1) { - var msg = "Данный сайт отсутствует в Списке доверенных узлов. (0xC2110F00)"; - untrustedSitesHandle(msg); - throw (msg); - } - return new window.ActiveXObject(name); - } - } - return pluginObject.CreateObject(name); - } - - function decimalToHexString(number) { - if (number < 0) { - number = 0xFFFFFFFF + number + 1; - } - - return number.toString(16).toUpperCase(); - } - - function GetMessageFromException(e) { - var err = e.message; - if (!err) { - err = e; - } else if (e.number) { - err += " (0x" + decimalToHexString(e.number) + ")"; - } - return err; - } - - function getLastError(exception) { - if (isNativeMessageSupported() || isIE() || isIOS()) { - return GetMessageFromException(exception); - } - - try { - return pluginObject.getLastError(); - } catch (e) { - return GetMessageFromException(exception); - } - } - - function ReleasePluginObjects() { - return cpcsp_chrome_nmcades.ReleasePluginObjects(); - } - - function CreateObjectAsync(name) { - return pluginObject.CreateObjectAsync(name); - } - - var ru_cryptopro_npcades_10_native_bridge = { - callbacksCount: 1, - callbacks: {}, - - resultForCallback: function resultForCallback(callbackId, resultArray) { - var callback = ru_cryptopro_npcades_10_native_bridge.callbacks[callbackId]; - if (!callback) { - return; - } - callback.apply(null, resultArray); - }, - - call: function call(functionName, args, callback) { - var hasCallback = callback && typeof callback === "function"; - var callbackId = hasCallback ? ru_cryptopro_npcades_10_native_bridge.callbacksCount++ : 0; - - if (hasCallback) { - ru_cryptopro_npcades_10_native_bridge.callbacks[callbackId] = callback; - } - - var iframe = document.createElement("IFRAME"); - var arrObjs = new Array("_CPNP_handle"); - try { - iframe.setAttribute("src", "cpnp-js-call:" + functionName + ":" + callbackId + ":" + encodeURIComponent(window.JSON.stringify(args, arrObjs))); - } catch (e) { - window.alert(e); - } - document.documentElement.appendChild(iframe); - iframe.parentNode.removeChild(iframe); - iframe = null; - } - }; - - function call_ru_cryptopro_npcades_10_native_bridge(functionName, array) { - var tmpobj; - var ex; - ru_cryptopro_npcades_10_native_bridge.call(functionName, array, function (e, response) { - ex = e; - var tmpobj = ""; - try { - tmpobj = window.JSON.parse(response); - } - catch (err) { - tmpobj = response; - } - if (typeof tmpobj === "string") { - tmpobj = tmpobj.replace(/\\\n/gm, "\n"); - tmpobj = tmpobj.replace(/\\\r/gm, "\r"); - } - }); - if (ex) { - throw ex; - } - return tmpobj; - } - - function show_firefox_missing_extension_dialog() { - if (!window.cadesplugin_skip_extension_install) { - var ovr = document.createElement('div'); - ovr.id = "cadesplugin_ovr"; - ovr.style = "visibility: hidden; position: fixed; left: 0; top: 0; width:100%; height:100%; background-color: rgba(0,0,0,0.7)"; - ovr.innerHTML = "
" + - "" + - "

Для работы КриптоПро ЭЦП Browser plugin на данном сайте необходимо расширение для браузера. Убедитесь, что оно у Вас включено или установите его." + - "

Скачать расширение

" + - "
"; - document.getElementsByTagName("Body")[0].appendChild(ovr); - document.getElementById("cadesplugin_close_install").addEventListener('click', function () { - plugin_loaded_error("Плагин недоступен"); - document.getElementById("cadesplugin_ovr").style.visibility = 'hidden'; - }); - - ovr.addEventListener('click', function () { - plugin_loaded_error("Плагин недоступен"); - document.getElementById("cadesplugin_ovr").style.visibility = 'hidden'; - }); - ovr.style.visibility = "visible"; - } - } - - function firefox_or_safari_nmcades_onload() { - if (window.cadesplugin_extension_loaded_callback) { - window.cadesplugin_extension_loaded_callback(); - } - isFireFoxExtensionLoaded = true; - cpcsp_chrome_nmcades.check_chrome_plugin(plugin_loaded, plugin_loaded_error); - } - - function load_js_script(url, successFunc, errorFunc) { - var script = document.createElement("script"); - script.setAttribute("type", "text/javascript"); - script.setAttribute("src", url); - script.onerror = errorFunc; - script.onload = successFunc; - document.getElementsByTagName("head")[0].appendChild(script); - } - - function nmcades_api_onload() { - if (!isIE() && !isFireFox && !isSafari) { - if (window.cadesplugin_extension_loaded_callback) { - window.cadesplugin_extension_loaded_callback(); - } - } - window.postMessage("cadesplugin_echo_request", "*"); - window.addEventListener("message", function (event) { - if (typeof (event.data) !== "string" || !event.data.match("cadesplugin_loaded")) { - return; - } - if (cadesplugin_loaded_event_recieved) { - return; - } - if (isFireFox || isSafari) { - var url = event.data.substring(event.data.indexOf("url:") + 4); - if (!url.match("^(moz|safari)-extension://[a-zA-Z0-9/_-]+/nmcades_plugin_api.js$")) { - cpcsp_console_log(cadesplugin.LOG_LEVEL_ERROR, "Bad url \"" + url + "\" for load CryptoPro Extension for CAdES Browser plug-in"); - plugin_loaded_error(); - return; - } - load_js_script(url, firefox_or_safari_nmcades_onload, plugin_loaded_error); - } else { - cpcsp_chrome_nmcades.check_chrome_plugin(plugin_loaded, plugin_loaded_error); - } - cadesplugin_loaded_event_recieved = true; - }, false); - } - - function load_extension() { - if (isFireFox || isSafari) { - nmcades_api_onload(); - return; - } - var operaUrl = "chrome-extension://epebfcehmdedogndhlcacafjaacknbcm/nmcades_plugin_api.js"; - var manifestv2Url = "chrome-extension://iifchhfnnmpdbibifmljnfjhpififfog/nmcades_plugin_api.js"; - var manifestv3Url = "chrome-extension://pfhgbfnnjiafkhfdkmpiflachepdcjod/nmcades_plugin_api.js"; - if (isYandex) { - load_js_script(operaUrl, nmcades_api_onload, function () { - load_js_script(manifestv2Url, nmcades_api_onload, function () { - load_js_script(manifestv3Url, nmcades_api_onload, plugin_loaded_error); - }); - }); - return; - } - if (isOpera) { - load_js_script(manifestv2Url, nmcades_api_onload, function () { - load_js_script(operaUrl, nmcades_api_onload, function () { - load_js_script(manifestv3Url, nmcades_api_onload, plugin_loaded_error); - }); - }); - return; - } - load_js_script(manifestv2Url, nmcades_api_onload, function () { - load_js_script(manifestv3Url, nmcades_api_onload, plugin_loaded_error); - }); - } - - function load_npapi_plugin() { - var elem = document.createElement('object'); - elem.setAttribute("id", "cadesplugin_object"); - elem.setAttribute("type", "application/x-cades"); - elem.setAttribute("style", "visibility: hidden"); - document.getElementsByTagName("body")[0].appendChild(elem); - pluginObject = document.getElementById("cadesplugin_object"); - if (isIE()) { - var elem1 = document.createElement('object'); - elem1.setAttribute("id", "certEnrollClassFactory"); - elem1.setAttribute("classid", "clsid:884e2049-217d-11da-b2a4-000e7bbb2b09"); - elem1.setAttribute("style", "visibility: hidden"); - document.getElementsByTagName("body")[0].appendChild(elem1); - var elem2 = document.createElement('object'); - elem2.setAttribute("id", "webClassFactory"); - elem2.setAttribute("classid", "clsid:B04C8637-10BD-484E-B0DA-B8A039F60024"); - elem2.setAttribute("style", "visibility: hidden"); - document.getElementsByTagName("body")[0].appendChild(elem2); - } - } - - function plugin_loaded() { - plugin_resolved = 1; - if (window.cadesplugin_plugin_loaded_callback) - window.cadesplugin_plugin_loaded_callback(); - if (canPromise) { - plugin_resolve(); - } else { - window.postMessage("cadesplugin_loaded", "*"); - } - } - - function plugin_loaded_error(msg) { - if (typeof (msg) === 'undefined' || typeof (msg) === 'object') { - msg = "Плагин недоступен"; - } - plugin_resolved = 1; - if (canPromise) { - plugin_reject(msg); - } else { - window.postMessage("cadesplugin_load_error", "*"); - } - } - - function check_load_timeout() { - if (plugin_resolved === 1) { - return; - } - if (isFireFox && !isFireFoxExtensionLoaded) { - show_firefox_missing_extension_dialog(); - } - plugin_resolved = 1; - if (window.cadesplugin_timeout_failed_callback) - window.cadesplugin_timeout_failed_callback(); - if (canPromise) { - plugin_reject("Истекло время ожидания загрузки плагина"); - } else { - window.postMessage("cadesplugin_load_error", "*"); - } - } - - function check_npapi_plugin() { - try { - CreateObject("CAdESCOM.About"); - plugin_loaded(); - } catch (err) { - document.getElementById("cadesplugin_object").style.display = 'none'; - var mimetype = window.navigator.mimeTypes["application/x-cades"]; - if (mimetype) { - var plugin = mimetype.enabledPlugin; - if (plugin) { - plugin_loaded_error("Плагин загружен, но не создаются обьекты"); - } else { - plugin_loaded_error("Ошибка при загрузке плагина"); - } - } else { - plugin_loaded_error("Плагин недоступен"); - } - } - } - - function check_plugin_working() { - var div = document.createElement("div"); - div.innerHTML = ""; - var isIeLessThan9 = (div.getElementsByTagName("i").length === 1); - if (isIeLessThan9) { - plugin_loaded_error("Internet Explorer версии 8 и ниже не поддерживается"); - return; - } - - if (isNativeMessageSupported()) { - load_extension(); - } else if (!canPromise) { - window.addEventListener("message", function (event) { - if (event.data !== "cadesplugin_echo_request") { - return; - } - load_npapi_plugin(); - check_npapi_plugin(); - }, false); - } else { - if (document.readyState === "complete") { - load_npapi_plugin(); - check_npapi_plugin(); - } else { - window.addEventListener("load", function (event) { - load_npapi_plugin(); - check_npapi_plugin(); - }, false); - } - } - } - - function set_pluginObject(obj) { - pluginObject = obj; - } - - function is_capilite_enabled() { - return ((typeof (cadesplugin.EnableInternalCSP) !== 'undefined') && cadesplugin.EnableInternalCSP); - } - - function set_load_timeout() { - if (window.cadesplugin_load_timeout) { - window.setTimeout(check_load_timeout, window.cadesplugin_load_timeout); - } else { - window.setTimeout(check_load_timeout, 20000); - } - } - - var onVisibilityChange = function (event) { - if (document.hidden === false) { - document.removeEventListener("visibilitychange", onVisibilityChange); - set_load_timeout(); - check_plugin_working(); - } - }; - - cadesplugin.JSModuleVersion = "2.4.5"; - cadesplugin.async_spawn = async_spawn; - cadesplugin.set = set_pluginObject; - cadesplugin.set_log_level = set_log_level; - cadesplugin.get_extension_version = get_extension_version; - cadesplugin.get_extension_id = get_extension_id; - cadesplugin.getLastError = getLastError; - cadesplugin.is_capilite_enabled = is_capilite_enabled; - - if (isNativeMessageSupported()) { - cadesplugin.CreateObjectAsync = CreateObjectAsync; - cadesplugin.ReleasePluginObjects = ReleasePluginObjects; - } - - if (!isNativeMessageSupported()) { - cadesplugin.CreateObject = CreateObject; - } - - set_constantValues(); - - cadesplugin.current_log_level = cadesplugin.LOG_LEVEL_ERROR; - window.cadesplugin = cadesplugin; - if (isSafari && document.hidden) { - document.addEventListener("visibilitychange", onVisibilityChange); - return; - } - set_load_timeout(); - check_plugin_working(); -}()); - diff --git a/docker-compose.yml b/docker-compose.yml index bafb9b6..1d05f41 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: - "3002:3001" env_file: - ./backend/.env + volumes: + - ./plans:/app/plans:ro networks: - chestny-znak-net diff --git a/frontend/package.json b/frontend/package.json index 90cf548..94721d2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c7c4f49..4f76392 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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('auth') const [authenticated, setAuthenticated] = useState(false) + const [env, setEnvState] = useState('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() { Загрузка УПД -
+
+ {authenticated ? '✅ Авторизован' : '❌ Не авторизован'} {authenticated && (
) diff --git a/frontend/src/pages/AuthPage.tsx b/frontend/src/pages/AuthPage.tsx index 674dc89..758f85e 100644 --- a/frontend/src/pages/AuthPage.tsx +++ b/frontend/src/pages/AuthPage.tsx @@ -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(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 && (
Ошибка: {error}
)} + + {errorDebug && ( +
+ + {debugExpanded && ( +
+                {errorDebug}
+              
+ )} +
+ )}
diff --git a/frontend/src/pages/CheckCodesPage.tsx b/frontend/src/pages/CheckCodesPage.tsx index 9c2330a..a827538 100644 --- a/frontend/src/pages/CheckCodesPage.tsx +++ b/frontend/src/pages/CheckCodesPage.tsx @@ -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(null) const [loading, setLoading] = useState(false) const [error, setError] = useState('') + const [errorDebug, setErrorDebug] = useState(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() {
Ошибка: {error}
)} + {errorDebug && ( +
+ + {debugExpanded && ( +
+              {errorDebug}
+            
+ )} +
+ )} + {results && (
diff --git a/frontend/src/pages/UploadUpdPage.tsx b/frontend/src/pages/UploadUpdPage.tsx index 53bd07c..704e82a 100644 --- a/frontend/src/pages/UploadUpdPage.tsx +++ b/frontend/src/pages/UploadUpdPage.tsx @@ -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(null) const [loading, setLoading] = useState(false) const [error, setError] = useState('') + const [errorDebug, setErrorDebug] = useState(null) + const [debugExpanded, setDebugExpanded] = useState(false) const [dragging, setDragging] = useState(false) const inputRef = useRef(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() {
Ошибка: {error}
)} + {errorDebug && ( +
+ + {debugExpanded && ( +
+              {errorDebug}
+            
+ )} +
+ )} + {response && (
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(url: string, options?: RequestInit): Promise { - const headers: Record = { '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 { + 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(url: string, options?: RequestInit & { env?: ApiEnv }): Promise { + const reqEnv = options?.env || currentEnv + const headers: Record = {} + 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) } - 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 { - return request('/auth/status') +export async function getAuthStatus(env?: ApiEnv): Promise { + return request('/auth/status', { env }) } -export async function logout(): Promise { - await request('/auth/logout', { method: 'POST' }) +export async function logout(env?: ApiEnv): Promise { + await request('/auth/logout', { method: 'POST', env }) } -export async function checkCodesPublic(codes: string[]): Promise { +export async function checkCodesPublic(codes: string[], env?: ApiEnv): Promise { return request('/api/check-codes/public', { method: 'POST', body: JSON.stringify({ codes }), + env, }) } -export async function checkCodesAuth(codes: string[]): Promise { +export async function checkCodesAuth(codes: string[], env?: ApiEnv): Promise { return request('/api/check-codes/auth', { method: 'POST', body: JSON.stringify({ codes }), + env, }) } -export async function uploadUpd(file: File): Promise { +export async function uploadUpd(file: File, env?: ApiEnv): Promise { 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}`) } diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index e91859c..31adfd8 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -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 + body: unknown + } + response: { + statusCode: number + headers: Record + body: unknown + } +} + +export interface ApiErrorResponse { + error: string + debugInfo?: ApiErrorLog +} + export interface UpdUploadResponse { document: UpdDocument codesFound: number diff --git a/plans/chestny-znak-web-app-2026-07-08.md b/plans/chestny-znak-web-app-2026-07-08.md index d6dc27c..b36657b 100644 --- a/plans/chestny-znak-web-app-2026-07-08.md +++ b/plans/chestny-znak-web-app-2026-07-08.md @@ -142,6 +142,17 @@ GET https://mobile.api.crpt.ru/mobile/check?code=0104640507713421215TxdVFoOB.zFF - 8.3. `docker-compose.yml` — сервисы `backend`, `frontend`, общая сеть - 8.4. README с инструкцией по запуску (`docker-compose up --build`) +- [x] 9. **Переключение контура (sandbox/prod) из UI** + - 9.1. Backend: два набора URL (`envUrls`) в `config.ts`, per-env кеш токенов + - 9.2. Backend: все сервисы и роуты принимают `?env=sandbox|prod` + - 9.3. Frontend: `