fix: harden auth and investment synchronization
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Successful in 11m9s
Conventional commits / commits (pull_request) Successful in 28s
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 10m38s

Resolve A01-A10 and C01-C07 from the backend audit.
Preserve auth failures, forced candle reconciliation and forecast freshness.
Validate MOEX responses and nested HTTP errors; remove unused legacy paths.

Verified: 209 unit tests, 262 SQLite e2e tests on Linux, OpenAPI and durability.
Record the remaining macOS native SQLite abort in the audit.
This commit is contained in:
2026-09-27 14:09:05 +03:00
parent 5d3835e796
commit 4796d9d3f0
55 changed files with 1954 additions and 569 deletions
@@ -10,7 +10,6 @@ import {
Req,
UseGuards,
ValidationPipe,
type ValidationError,
} from '@nestjs/common';
import {
ApiAcceptedResponse,
@@ -58,8 +57,8 @@ export class TbankSyncController {
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
exceptionFactory: (message) =>
new BadRequestException({ message: descriptorErrors(message) }),
validationError: { target: false, value: false },
exceptionFactory: (message) => new BadRequestException({ message }),
}),
)
input: unknown,
@@ -75,13 +74,3 @@ export class TbankSyncController {
return this.sync.status(request.user.sub, id);
}
}
function descriptorErrors(
errors: ValidationError[],
prefix = '',
): ValidationError[] {
return errors.flatMap((error) => [
{ ...error, property: `${prefix}${error.property}` },
...descriptorErrors(error.children ?? [], `${prefix}${error.property}/`),
]);
}
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { createHash, randomUUID } from 'node:crypto';
import type { Prisma } from '../../../../generated/prisma/client';
import { PrismaService } from '../../../prisma/prisma.service';
import type { TbankSyncAttempt } from '../../../integrations/tbank/tbank-access.service';
import type { TbankSyncAttempt } from '../../../integrations/tbank/tbank.api';
import type { InvestmentScreenDto } from './tbank-sync.dto';
export type ScreenIntent = { scope: InvestmentScreenDto; now: string };
export type ScreenProgress = {
@@ -9,6 +9,7 @@ import {
TbankAccessService,
TbankMembershipService,
InstrumentMetadataService,
type TbankSyncAttempt,
} from '../../../integrations/tbank/tbank.api';
import { PortfolioService } from '../portfolio/portfolio.api';
import {
@@ -30,7 +31,6 @@ import { IncomeAnalyticsService } from '../income/income-read.api';
import { UpcomingEventsService } from '../upcoming-events/upcoming-events-read.api';
import { planInvestmentScreen } from '../investment-sync-plan';
import type { InvestmentResourceMetaDto } from '../investment-resource.dto';
import type { TbankSyncAttempt } from '../../../integrations/tbank/tbank-access.service';
import {
TbankSyncRepository,
type ScreenIntent,
@@ -303,6 +303,7 @@ export class TbankSyncService {
},
now,
true,
0,
),
]);
}
@@ -327,10 +328,17 @@ export class TbankSyncService {
return this.sourceStatus(userId, saved.work);
const intent = saved.work.intent as unknown as ScreenIntent;
await this.access.local(userId, intent.scope.accountId);
const running = saved.children.some((child) => child.state === 'running');
const children = await Promise.all(
saved.children.map((child) =>
child.state === 'superseded'
? this.operations.observe(userId, child.id)
: Promise.resolve(child),
),
);
const running = children.some((child) => child.state === 'running');
const failed =
saved.work.errorCode ??
saved.children.find((child) => child.errorCode)?.errorCode ??
children.find((child) => child.errorCode)?.errorCode ??
null;
const state =
saved.work.processRunId !== this.runId &&
@@ -340,7 +348,7 @@ export class TbankSyncService {
? 'running'
: failed
? 'failed'
: saved.children.some((child) => child.state === 'cancelled')
: children.some((child) => child.state === 'cancelled')
? 'cancelled'
: 'succeeded';
const resources = await this.observe(userId, intent.scope);
@@ -355,9 +363,8 @@ export class TbankSyncService {
state,
errorCode: failed,
progress: {
completed: saved.children.filter((child) => child.state !== 'running')
.length,
total: saved.children.length,
completed: children.filter((child) => child.state !== 'running').length,
total: children.length,
discovering: !saved.progress.dispatched,
},
resources,