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
+20 -24
View File
@@ -29,9 +29,8 @@ export class AuthService {
email: string,
password: string,
): Promise<{ user: AuthUser; accessToken: string; refreshToken: string }> {
const normalizedEmail = this.normalizeEmail(email);
const existing = await this.prisma.user.findUnique({
where: { email: normalizedEmail },
where: { email },
});
if (existing)
throw new ConflictException({ code: 'EMAIL_ALREADY_REGISTERED' });
@@ -41,7 +40,7 @@ export class AuthService {
user = await this.prisma.user.create({
data: {
id: randomUUID(),
email: normalizedEmail,
email,
passwordHash: await bcrypt.hash(password, PASSWORD_HASH_ROUNDS),
},
});
@@ -58,7 +57,7 @@ export class AuthService {
password: string,
): Promise<{ user: AuthUser; accessToken: string; refreshToken: string }> {
const user = await this.prisma.user.findUnique({
where: { email: this.normalizeEmail(email) },
where: { email },
});
const valid = await bcrypt.compare(
password,
@@ -103,26 +102,27 @@ export class AuthService {
async logout(token?: string): Promise<void> {
if (!token) return;
let payload: RefreshPayload;
try {
const payload = await this.jwt.verifyAsync<RefreshPayload>(token, {
payload = await this.jwt.verifyAsync<RefreshPayload>(token, {
algorithms: ['HS256'],
});
if (payload.type !== 'refresh') return;
const session = await this.prisma.session.findUnique({
where: { id: payload.sid },
});
if (
session &&
!session.revokedAt &&
session.refreshTokenHash === this.hash(token)
) {
await this.prisma.session.update({
where: { id: session.id },
data: { revokedAt: new Date() },
});
}
} catch {
// Logout is intentionally idempotent.
return;
}
if (payload.type !== 'refresh') return;
const session = await this.prisma.session.findUnique({
where: { id: payload.sid },
});
if (
session &&
!session.revokedAt &&
session.refreshTokenHash === this.hash(token)
) {
await this.prisma.session.update({
where: { id: session.id },
data: { revokedAt: new Date() },
});
}
}
@@ -187,10 +187,6 @@ export class AuthService {
};
}
private normalizeEmail(email: string): string {
return email;
}
private hash(token: string): string {
return createHash('sha256').update(token).digest('hex');
}
@@ -1,21 +0,0 @@
import { HTTP_CODE_METADATA } from '@nestjs/common/constants';
jest.mock('../application/auth.service', () => ({
AuthService: class AuthService {},
}));
import { AuthController } from './auth.controller';
describe('AuthController response statuses', () => {
it.each(['register', 'login', 'refresh'] as const)(
'%s explicitly returns HTTP 200',
(handler) => {
// Nest stores @HttpCode metadata on the unbound handler function.
const handlerTarget = Object.getOwnPropertyDescriptor(
AuthController.prototype,
handler,
)?.value as object;
expect(Reflect.getMetadata(HTTP_CODE_METADATA, handlerTarget)).toBe(200);
},
);
});
@@ -87,7 +87,7 @@ export class AuthController {
if (!token) throw new UnauthorizedException({ code: 'UNAUTHORIZED' });
return this.respond(response, await this.auth.refresh(token));
} catch (error) {
this.clearCookie(response);
if (error instanceof UnauthorizedException) this.clearCookie(response);
throw error;
}
}