[105-9][@ant] refactor: перевести account self-service auth-v2 с REST на GraphQL/SDK — единый типизированный фасад фронта
Корзина C Фазы 2: sessions/2fa/recovery-strategy/security-not-me → AccountSecurityResolver (9 операций, GqlJwtAuthGuard), а critical-actions/keys/force-recovery → CriticalActionsResolver (6 операций, тот же CASL @CheckAbility+AuthorizationGuard). Движок Эпика 6 не тронут — резолверы поверх тех же сервисов. 5 REST-контролёров сняты целиком, 2 урезаны до magic-link :token (корзина D). SDK-домены AccountSecurity/CriticalActions + codegen (SDL 62 резолвера) + Zeus закоммичен. Транспорт (IP/refresh-токен) — request-meta декораторы, не GraphQL-переменные. Unit 24/24, ESLint 0. Зачем: наружу на фронт смотрит только @coopenomics/sdk — нового способа взаимодействия с бэкендом не появляется, bearer живёт только в SDK. OIDC (.well-known), webhook (coop/internal) и login-контур (Фаза 3) остаются REST. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+135
@@ -0,0 +1,135 @@
|
||||
import type { SessionsService } from '../sessions/sessions.service';
|
||||
import type { TwoFactorService } from '../two-factor/two-factor.service';
|
||||
import type { RecoveryStrategyService } from '../recovery/recovery-strategy.service';
|
||||
import type { SecurityIncidentService } from '../security/security-incident.service';
|
||||
import { RecoveryStrategy } from '~/domain/auth-v2/recovery-strategy/recovery-strategy.types';
|
||||
import { AccountSecurityResolver } from './account-security.resolver';
|
||||
|
||||
const USER = { id: 'u1', username: 'payer1', role: 'user' };
|
||||
const IP = '203.0.113.7';
|
||||
|
||||
function build() {
|
||||
const sessions = {
|
||||
list: jest.fn(async () => [
|
||||
{
|
||||
id: 's1',
|
||||
device: 'Firefox',
|
||||
ip: '10.0.0.1',
|
||||
createdAt: '2026-06-13T00:00:00Z',
|
||||
lastSeenAt: '2026-06-14T00:00:00Z',
|
||||
current: true,
|
||||
},
|
||||
]),
|
||||
revoke: jest.fn(async () => undefined),
|
||||
revokeAll: jest.fn(async () => ({ revoked: 3 })),
|
||||
};
|
||||
const twoFactor = {
|
||||
beginEnrollment: jest.fn(async () => ({ secret: 'BASE32SECRET', otpauthUri: 'otpauth://totp/x' })),
|
||||
activate: jest.fn(async () => undefined),
|
||||
disable: jest.fn(async () => undefined),
|
||||
};
|
||||
const recoveryStrategy = {
|
||||
getStrategy: jest.fn(async () => RecoveryStrategy.OfflineCode),
|
||||
setStrategy: jest.fn(async () => undefined),
|
||||
};
|
||||
const incidents = {
|
||||
report: jest.fn(async () => ({ revoked: 2 })),
|
||||
};
|
||||
const resolver = new AccountSecurityResolver(
|
||||
sessions as unknown as SessionsService,
|
||||
twoFactor as unknown as TwoFactorService,
|
||||
recoveryStrategy as unknown as RecoveryStrategyService,
|
||||
incidents as unknown as SecurityIncidentService,
|
||||
);
|
||||
return { resolver, sessions, twoFactor, recoveryStrategy, incidents };
|
||||
}
|
||||
|
||||
describe('AccountSecurityResolver', () => {
|
||||
it('getSessions маппит сессии в snake_case и передаёт refresh-токен текущей сессии', async () => {
|
||||
const { resolver, sessions } = build();
|
||||
const out = await resolver.getSessions(USER, 'rt-current');
|
||||
expect(sessions.list).toHaveBeenCalledWith('u1', 'rt-current');
|
||||
expect(out).toEqual([
|
||||
{
|
||||
id: 's1',
|
||||
device: 'Firefox',
|
||||
ip: '10.0.0.1',
|
||||
created_at: '2026-06-13T00:00:00Z',
|
||||
last_seen_at: '2026-06-14T00:00:00Z',
|
||||
current: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('getRecoveryStrategy возвращает стратегию пайщика', async () => {
|
||||
const { resolver, recoveryStrategy } = build();
|
||||
const out = await resolver.getRecoveryStrategy(USER);
|
||||
expect(recoveryStrategy.getStrategy).toHaveBeenCalledWith('u1');
|
||||
expect(out).toBe(RecoveryStrategy.OfflineCode);
|
||||
});
|
||||
|
||||
it('revokeSession завершает сессию по id с IP для аудита', async () => {
|
||||
const { resolver, sessions } = build();
|
||||
const ok = await resolver.revokeSession({ session_id: 's1' }, USER, IP);
|
||||
expect(ok).toBe(true);
|
||||
expect(sessions.revoke).toHaveBeenCalledWith('u1', 's1', IP);
|
||||
});
|
||||
|
||||
it('revokeAllSessions возвращает число завершённых', async () => {
|
||||
const { resolver, sessions } = build();
|
||||
const out = await resolver.revokeAllSessions(USER, IP);
|
||||
expect(sessions.revokeAll).toHaveBeenCalledWith('u1', IP);
|
||||
expect(out).toEqual({ revoked: 3 });
|
||||
});
|
||||
|
||||
it('enrollTwoFactor маппит otpauthUri в otpauth_uri', async () => {
|
||||
const { resolver, twoFactor } = build();
|
||||
const out = await resolver.enrollTwoFactor(USER);
|
||||
expect(twoFactor.beginEnrollment).toHaveBeenCalledWith('u1', 'payer1');
|
||||
expect(out).toEqual({ secret: 'BASE32SECRET', otpauth_uri: 'otpauth://totp/x' });
|
||||
});
|
||||
|
||||
it('activateTwoFactor подтверждает второй фактор кодом', async () => {
|
||||
const { resolver, twoFactor } = build();
|
||||
const ok = await resolver.activateTwoFactor({ code: '123456' }, USER, IP);
|
||||
expect(ok).toBe(true);
|
||||
expect(twoFactor.activate).toHaveBeenCalledWith('u1', '123456', IP);
|
||||
});
|
||||
|
||||
it('disableTwoFactor отключает второй фактор кодом', async () => {
|
||||
const { resolver, twoFactor } = build();
|
||||
const ok = await resolver.disableTwoFactor({ code: '654321' }, USER, IP);
|
||||
expect(ok).toBe(true);
|
||||
expect(twoFactor.disable).toHaveBeenCalledWith('u1', '654321', IP);
|
||||
});
|
||||
|
||||
it('setRecoveryStrategy меняет стратегию со step-up кодом', async () => {
|
||||
const { resolver, recoveryStrategy } = build();
|
||||
const ok = await resolver.setRecoveryStrategy({ strategy: RecoveryStrategy.Council, code: '000111' }, USER, IP);
|
||||
expect(ok).toBe(true);
|
||||
expect(recoveryStrategy.setStrategy).toHaveBeenCalledWith('u1', RecoveryStrategy.Council, '000111', IP);
|
||||
});
|
||||
|
||||
it('reportNotMe зовёт report с source=settings и подозрительной сессией', async () => {
|
||||
const { resolver, incidents } = build();
|
||||
const out = await resolver.reportNotMe({ session_id: 's9' }, USER, IP);
|
||||
expect(incidents.report).toHaveBeenCalledWith({
|
||||
subjectId: 'u1',
|
||||
ip: IP,
|
||||
source: 'settings',
|
||||
reportedSessionId: 's9',
|
||||
});
|
||||
expect(out).toEqual({ revoked: 2 });
|
||||
});
|
||||
|
||||
it('reportNotMe без session_id передаёт reportedSessionId=null', async () => {
|
||||
const { resolver, incidents } = build();
|
||||
await resolver.reportNotMe({}, USER, IP);
|
||||
expect(incidents.report).toHaveBeenCalledWith({
|
||||
subjectId: 'u1',
|
||||
ip: IP,
|
||||
source: 'settings',
|
||||
reportedSessionId: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
|
||||
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
|
||||
import { ClientIp, RefreshTokenHeader } from '~/application/auth/decorators/request-meta.decorator';
|
||||
import { RecoveryStrategy } from '~/domain/auth-v2/recovery-strategy/recovery-strategy.types';
|
||||
import { SessionsService } from '../sessions/sessions.service';
|
||||
import { TwoFactorService } from '../two-factor/two-factor.service';
|
||||
import { RecoveryStrategyService } from '../recovery/recovery-strategy.service';
|
||||
import { SecurityIncidentService } from '../security/security-incident.service';
|
||||
import {
|
||||
AccountSessionDTO,
|
||||
ReportNotMeInputDTO,
|
||||
RevokeSessionInputDTO,
|
||||
RevokedSessionsResultDTO,
|
||||
SetRecoveryStrategyInputDTO,
|
||||
TwoFactorCodeInputDTO,
|
||||
TwoFactorEnrollmentDTO,
|
||||
} from './dto/account-security.dto';
|
||||
|
||||
interface ICurrentUser {
|
||||
id: string;
|
||||
username: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL-фасад самообслуживания безопасности аккаунта (Фаза 2 миграции REST→GraphQL/SDK).
|
||||
* Заменяет REST-контроллеры `coop/sessions`, `coop/2fa`, `coop/recovery/strategy` и
|
||||
* JWT-метод `coop/security/not-me` — фронт ходит через @coopenomics/sdk (Zeus), нового
|
||||
* способа взаимодействия с бэкендом наружу не появляется.
|
||||
*
|
||||
* Все операции — для текущего залогиненного пайщика (subject = `user.id`) под
|
||||
* `GqlJwtAuthGuard`. IP и refresh-токен текущей сессии — транспорт, берутся из
|
||||
* request-meta декораторов, не из GraphQL-переменных.
|
||||
*/
|
||||
@Resolver()
|
||||
export class AccountSecurityResolver {
|
||||
constructor(
|
||||
private readonly sessions: SessionsService,
|
||||
private readonly twoFactor: TwoFactorService,
|
||||
private readonly recoveryStrategy: RecoveryStrategyService,
|
||||
private readonly incidents: SecurityIncidentService,
|
||||
) {}
|
||||
|
||||
@Query(() => [AccountSessionDTO], {
|
||||
name: 'getSessions',
|
||||
description: 'Активные сессии текущего пайщика (текущая помечается current)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async getSessions(
|
||||
@CurrentUser() user: ICurrentUser,
|
||||
@RefreshTokenHeader() currentRefreshToken: string | null,
|
||||
): Promise<AccountSessionDTO[]> {
|
||||
const sessions = await this.sessions.list(user.id, currentRefreshToken);
|
||||
return sessions.map((s) => ({
|
||||
id: s.id,
|
||||
device: s.device,
|
||||
ip: s.ip,
|
||||
created_at: s.createdAt,
|
||||
last_seen_at: s.lastSeenAt,
|
||||
current: s.current,
|
||||
}));
|
||||
}
|
||||
|
||||
@Query(() => RecoveryStrategy, {
|
||||
name: 'getRecoveryStrategy',
|
||||
description: 'Текущая стратегия восстановления доступа пайщика',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async getRecoveryStrategy(@CurrentUser() user: ICurrentUser): Promise<RecoveryStrategy> {
|
||||
return this.recoveryStrategy.getStrategy(user.id);
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
name: 'revokeSession',
|
||||
description: 'Завершить конкретную сессию пайщика',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async revokeSession(
|
||||
@Args('data', { type: () => RevokeSessionInputDTO }) data: RevokeSessionInputDTO,
|
||||
@CurrentUser() user: ICurrentUser,
|
||||
@ClientIp() ip: string | null,
|
||||
): Promise<boolean> {
|
||||
await this.sessions.revoke(user.id, data.session_id, ip);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => RevokedSessionsResultDTO, {
|
||||
name: 'revokeAllSessions',
|
||||
description: 'Завершить все сессии пайщика',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async revokeAllSessions(
|
||||
@CurrentUser() user: ICurrentUser,
|
||||
@ClientIp() ip: string | null,
|
||||
): Promise<RevokedSessionsResultDTO> {
|
||||
return this.sessions.revokeAll(user.id, ip);
|
||||
}
|
||||
|
||||
@Mutation(() => TwoFactorEnrollmentDTO, {
|
||||
name: 'enrollTwoFactor',
|
||||
description: 'Начать подключение второго фактора: выпустить секрет и otpauth-URI для QR',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async enrollTwoFactor(@CurrentUser() user: ICurrentUser): Promise<TwoFactorEnrollmentDTO> {
|
||||
const challenge = await this.twoFactor.beginEnrollment(user.id, user.username);
|
||||
return { secret: challenge.secret, otpauth_uri: challenge.otpauthUri };
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
name: 'activateTwoFactor',
|
||||
description: 'Подтвердить подключение второго фактора первым кодом',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async activateTwoFactor(
|
||||
@Args('data', { type: () => TwoFactorCodeInputDTO }) data: TwoFactorCodeInputDTO,
|
||||
@CurrentUser() user: ICurrentUser,
|
||||
@ClientIp() ip: string | null,
|
||||
): Promise<boolean> {
|
||||
await this.twoFactor.activate(user.id, data.code, ip);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
name: 'disableTwoFactor',
|
||||
description: 'Отключить второй фактор (требует валидный код)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async disableTwoFactor(
|
||||
@Args('data', { type: () => TwoFactorCodeInputDTO }) data: TwoFactorCodeInputDTO,
|
||||
@CurrentUser() user: ICurrentUser,
|
||||
@ClientIp() ip: string | null,
|
||||
): Promise<boolean> {
|
||||
await this.twoFactor.disable(user.id, data.code, ip);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
name: 'setRecoveryStrategy',
|
||||
description: 'Сменить стратегию восстановления (требует step-up второго фактора)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async setRecoveryStrategy(
|
||||
@Args('data', { type: () => SetRecoveryStrategyInputDTO }) data: SetRecoveryStrategyInputDTO,
|
||||
@CurrentUser() user: ICurrentUser,
|
||||
@ClientIp() ip: string | null,
|
||||
): Promise<boolean> {
|
||||
await this.recoveryStrategy.setStrategy(user.id, data.strategy, data.code, ip);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => RevokedSessionsResultDTO, {
|
||||
name: 'reportNotMe',
|
||||
description: 'Сигнал «Это не я»: немедленно завершить все сессии пайщика',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async reportNotMe(
|
||||
@Args('data', { type: () => ReportNotMeInputDTO }) data: ReportNotMeInputDTO,
|
||||
@CurrentUser() user: ICurrentUser,
|
||||
@ClientIp() ip: string | null,
|
||||
): Promise<RevokedSessionsResultDTO> {
|
||||
return this.incidents.report({
|
||||
subjectId: user.id,
|
||||
ip,
|
||||
source: 'settings',
|
||||
reportedSessionId: data.session_id ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { Field, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
import { RecoveryStrategy } from '~/domain/auth-v2/recovery-strategy/recovery-strategy.types';
|
||||
|
||||
/**
|
||||
* GraphQL-контракт самообслуживания безопасности аккаунта пайщика (Фаза 2 миграции
|
||||
* REST→GraphQL/SDK): активные сессии (Story 3.7), второй фактор (3.6), стратегия
|
||||
* восстановления (3.5), сигнал «Это не я» (3.10). Зеркало доменных типов auth-v2,
|
||||
* поля — snake_case по канону GraphQL этого репозитория; резолвер маппит camelCase
|
||||
* сервисов в snake_case этих DTO.
|
||||
*
|
||||
* Зачем GraphQL, а не REST: единый типизированный фасад фронта — @coopenomics/sdk
|
||||
* (Zeus). Нового способа взаимодействия с бэкендом наружу не появляется; bearer
|
||||
* живёт только в SDK. Транспорт (IP, refresh-токен текущей сессии) — через
|
||||
* request-meta декораторы, не как GraphQL-переменные.
|
||||
*/
|
||||
|
||||
registerEnumType(RecoveryStrategy, {
|
||||
name: 'RecoveryStrategy',
|
||||
description: 'Разрешённый канал восстановления доступа пайщика (активен ровно один)',
|
||||
});
|
||||
|
||||
/** Активная сессия пайщика — устройство, с которого выполнен вход. */
|
||||
@ObjectType('AccountSession')
|
||||
export class AccountSessionDTO {
|
||||
@Field(() => String, { description: 'Идентификатор сессии (для точечного завершения)' })
|
||||
id!: string;
|
||||
|
||||
@Field(() => String, { description: 'Устройство входа (User-Agent); заглушка, если метаданные не сохранялись' })
|
||||
device!: string;
|
||||
|
||||
@Field(() => String, { description: 'IP входа; заглушка, если метаданные не сохранялись' })
|
||||
ip!: string;
|
||||
|
||||
@Field(() => String, { description: 'Время создания сессии (ISO)' })
|
||||
created_at!: string;
|
||||
|
||||
@Field(() => String, { description: 'Последняя зафиксированная активность (ISO)' })
|
||||
last_seen_at!: string;
|
||||
|
||||
@Field(() => Boolean, { description: 'Текущая сессия (с которой выполнен запрос)' })
|
||||
current!: boolean;
|
||||
}
|
||||
|
||||
/** Результат массового завершения сессий. */
|
||||
@ObjectType('RevokedSessionsResult')
|
||||
export class RevokedSessionsResultDTO {
|
||||
@Field(() => Int, { description: 'Сколько активных сессий завершено' })
|
||||
revoked!: number;
|
||||
}
|
||||
|
||||
/** Вызов на подключение второго фактора: данные для ручного ввода и QR. */
|
||||
@ObjectType('TwoFactorEnrollment')
|
||||
export class TwoFactorEnrollmentDTO {
|
||||
@Field(() => String, { description: 'Base32-секрет для ручного ввода в приложение-аутентификатор' })
|
||||
secret!: string;
|
||||
|
||||
@Field(() => String, { description: 'otpauth://-URI для QR-кода' })
|
||||
otpauth_uri!: string;
|
||||
}
|
||||
|
||||
/** Вход на завершение конкретной сессии. */
|
||||
@InputType('RevokeSessionInput')
|
||||
export class RevokeSessionInputDTO {
|
||||
@Field(() => String, { description: 'Идентификатор завершаемой сессии' })
|
||||
session_id!: string;
|
||||
}
|
||||
|
||||
/** Вход на операции второго фактора, требующие TOTP-кода (активация/отключение). */
|
||||
@InputType('TwoFactorCodeInput')
|
||||
export class TwoFactorCodeInputDTO {
|
||||
@Field(() => String, { description: 'Одноразовый код из приложения-аутентификатора' })
|
||||
code!: string;
|
||||
}
|
||||
|
||||
/** Вход на смену стратегии восстановления (требует step-up второго фактора). */
|
||||
@InputType('SetRecoveryStrategyInput')
|
||||
export class SetRecoveryStrategyInputDTO {
|
||||
@Field(() => RecoveryStrategy, { description: 'Новая стратегия восстановления' })
|
||||
strategy!: RecoveryStrategy;
|
||||
|
||||
@Field(() => String, { description: 'TOTP-код для подтверждения смены (step-up)' })
|
||||
code!: string;
|
||||
}
|
||||
|
||||
/** Вход на сигнал «Это не я» из настроек ЛК. */
|
||||
@InputType('ReportNotMeInput')
|
||||
export class ReportNotMeInputDTO {
|
||||
@Field(() => String, { nullable: true, description: 'Идентификатор подозрительной сессии (опционально, из настроек)' })
|
||||
session_id?: string | null;
|
||||
}
|
||||
@@ -32,23 +32,20 @@ import { RecoveryService } from './recovery/recovery.service';
|
||||
import { RecoveryConfirmService } from './recovery/recovery-confirm.service';
|
||||
import { OfflineRecoveryService } from './recovery/offline-recovery.service';
|
||||
import { RecoveryStrategyService } from './recovery/recovery-strategy.service';
|
||||
import { RecoveryStrategyController } from './recovery/recovery-strategy.controller';
|
||||
import { RecoveryFinalizationService } from './recovery/recovery-finalization.service';
|
||||
import { RecoveryController } from './recovery/recovery.controller';
|
||||
import { TwoFactorService } from './two-factor/two-factor.service';
|
||||
import { TwoFactorController } from './two-factor/two-factor.controller';
|
||||
import { SessionsService } from './sessions/sessions.service';
|
||||
import { SessionsController } from './sessions/sessions.controller';
|
||||
import { SecurityIncidentService } from './security/security-incident.service';
|
||||
import { SecurityIncidentController } from './security/security-incident.controller';
|
||||
import { CriticalActionsService } from './critical-actions/critical-actions.service';
|
||||
import { CriticalActionsController } from './critical-actions/critical-actions.controller';
|
||||
import { ForceRecoveryService } from './force-recovery/force-recovery.service';
|
||||
import { ForceRecoveryController } from './force-recovery/force-recovery.controller';
|
||||
import { KeyRevocationService } from './key-revocation/key-revocation.service';
|
||||
import { KeyRevocationController } from './key-revocation/key-revocation.controller';
|
||||
import { CapabilitySetService } from './authorization/capability-set.service';
|
||||
import { AuthorizationResolver } from './authorization/authorization.resolver';
|
||||
import { AccountSecurityResolver } from './account-security/account-security.resolver';
|
||||
import { CriticalActionsResolver } from './critical-actions/critical-actions.resolver';
|
||||
|
||||
/**
|
||||
* auth-v2 (CoopID): новый контур аутентификации. Живёт рядом с legacy `auth/`
|
||||
@@ -58,9 +55,12 @@ import { AuthorizationResolver } from './authorization/authorization.resolver';
|
||||
*/
|
||||
@Module({
|
||||
imports: [RedisModule, AuthV2InfrastructureModule, TokenApplicationModule, AuthorizationModule],
|
||||
controllers: [AuthentikEventsController, SessionBindingController, VaultController, VerifyTimestampController, CoopIdClaimsPolicyController, CoopIdSchemaPolicyController, LogoutController, RecoveryController, RecoveryStrategyController, TwoFactorController, SessionsController, SecurityIncidentController, CriticalActionsController, ForceRecoveryController, KeyRevocationController],
|
||||
// SecurityIncidentController/ForceRecoveryController остаются REST только ради magic-link
|
||||
// `:token`-эндпоинтов (клик из письма без SDK-контекста); их JWT-методы переведены в
|
||||
// GraphQL/SDK (AccountSecurityResolver/CriticalActionsResolver, Фаза 2 миграции).
|
||||
controllers: [AuthentikEventsController, SessionBindingController, VaultController, VerifyTimestampController, CoopIdClaimsPolicyController, CoopIdSchemaPolicyController, LogoutController, RecoveryController, SecurityIncidentController, ForceRecoveryController],
|
||||
providers: [
|
||||
AuditService, AuditActionInterceptor, SessionBindingService, VaultService, VerifyTimestampService, CertificateService, CertSettingsService, VerificationTypesService, VerificationRulesService, VerificationRuleGuard, LogoutService, AuthRateLimitGuard, RecoveryService, RecoveryConfirmService, OfflineRecoveryService, RecoveryStrategyService, RecoveryFinalizationService, TwoFactorService, DeviceTrackingService, NewDeviceNotificationService, SecurityEventNotificationService, SessionsService, SecurityIncidentService, CriticalActionsService, ForceRecoveryService, KeyRevocationService, CapabilitySetService, AuthorizationResolver, CertificateResolver,
|
||||
AuditService, AuditActionInterceptor, SessionBindingService, VaultService, VerifyTimestampService, CertificateService, CertSettingsService, VerificationTypesService, VerificationRulesService, VerificationRuleGuard, LogoutService, AuthRateLimitGuard, RecoveryService, RecoveryConfirmService, OfflineRecoveryService, RecoveryStrategyService, RecoveryFinalizationService, TwoFactorService, DeviceTrackingService, NewDeviceNotificationService, SecurityEventNotificationService, SessionsService, SecurityIncidentService, CriticalActionsService, ForceRecoveryService, KeyRevocationService, CapabilitySetService, AuthorizationResolver, CertificateResolver, AccountSecurityResolver, CriticalActionsResolver,
|
||||
// Узкий verifier-порт для потребителей (recovery Story 3.2, 2FA-вход) → тот же сервис.
|
||||
{ provide: TWO_FACTOR_VERIFIER, useExisting: TwoFactorService },
|
||||
// Финализация recovery (Story 3.3): ротация ключа через registrator::changekey + vault + отзыв сессий + аудит.
|
||||
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
import { BadRequestException, Body, Controller, Get, Param, Post, Req, UnauthorizedException, UseFilters, UseGuards } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard';
|
||||
import { CriticalActionType, type PendingCriticalAction } from '~/domain/auth-v2/ports/pending-critical-actions.port';
|
||||
import { AuthorizationGuard } from '../authorization/authorization.guard';
|
||||
import { CheckAbility } from '../authorization/check-ability.decorator';
|
||||
import { AuthV2ExceptionFilter } from '../exceptions/auth-v2-exception.filter';
|
||||
import { CriticalActionsService, type CriticalActionAuditEntry } from './critical-actions.service';
|
||||
|
||||
interface AuthedRequest extends Request {
|
||||
user?: { id: string; username: string; role?: string };
|
||||
}
|
||||
|
||||
interface InitiateBody {
|
||||
actionType: CriticalActionType;
|
||||
targetId: string;
|
||||
payload?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Critical actions (CoopID, Story 6.8) — первый боевой `@CheckAbility` + `AuthorizationGuard`
|
||||
* (Story 6.4/6.5): инициация под capability `create CriticalAction` (председатель),
|
||||
* подтверждение под `confirm CriticalAction` (член совета). Различимость подписантов,
|
||||
* кворум и окно — в сервисе.
|
||||
*/
|
||||
@Controller('coop/critical-actions')
|
||||
@UseFilters(AuthV2ExceptionFilter)
|
||||
export class CriticalActionsController {
|
||||
constructor(private readonly service: CriticalActionsService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(HttpJwtAuthGuard, AuthorizationGuard)
|
||||
@CheckAbility('create', 'CriticalAction')
|
||||
async initiate(@Body() body: InitiateBody, @Req() req: AuthedRequest): Promise<PendingCriticalAction> {
|
||||
const actorId = req.user?.username;
|
||||
if (!actorId) throw new UnauthorizedException();
|
||||
if (!(Object.values(CriticalActionType) as string[]).includes(body?.actionType))
|
||||
throw new BadRequestException('Недопустимый тип критического действия');
|
||||
if (!body?.targetId) throw new BadRequestException('targetId обязателен');
|
||||
return this.service.initiate({
|
||||
actionType: body.actionType,
|
||||
actorId,
|
||||
targetId: body.targetId,
|
||||
payload: body.payload,
|
||||
});
|
||||
}
|
||||
|
||||
@Post(':id/confirm')
|
||||
@UseGuards(HttpJwtAuthGuard, AuthorizationGuard)
|
||||
@CheckAbility('confirm', 'CriticalAction')
|
||||
async confirm(@Param('id') id: string, @Req() req: AuthedRequest): Promise<PendingCriticalAction> {
|
||||
const confirmerId = req.user?.username;
|
||||
if (!confirmerId) throw new UnauthorizedException();
|
||||
return this.service.confirm(id, confirmerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit-trail критических действий пайщика (Story 6.10): полная атрибуция
|
||||
* (инициатор + подтверждающие совета + payload_hash) для контролирующего органа.
|
||||
* Право чтения — `read CriticalAction` (член совета / председатель).
|
||||
*/
|
||||
@Get('audit-trail/:targetId')
|
||||
@UseGuards(HttpJwtAuthGuard, AuthorizationGuard)
|
||||
@CheckAbility('read', 'CriticalAction')
|
||||
async auditTrail(@Param('targetId') targetId: string): Promise<CriticalActionAuditEntry[]> {
|
||||
if (!targetId) throw new BadRequestException('targetId обязателен');
|
||||
return this.service.getAuditTrail(targetId);
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import type { CriticalActionsService } from './critical-actions.service';
|
||||
import type { KeyRevocationService } from '../key-revocation/key-revocation.service';
|
||||
import type { ForceRecoveryService } from '../force-recovery/force-recovery.service';
|
||||
import { CriticalActionStatus, CriticalActionType } from '~/domain/auth-v2/ports/pending-critical-actions.port';
|
||||
import { ForceRecoveryConsentVia } from '../force-recovery/force-recovery.service';
|
||||
import { CriticalActionsResolver } from './critical-actions.resolver';
|
||||
|
||||
const CHAIRMAN = { id: 'u1', username: 'chairman1', role: 'chairman' };
|
||||
const IP = '203.0.113.7';
|
||||
|
||||
const PENDING = {
|
||||
id: 'ca1',
|
||||
actionType: CriticalActionType.ExcludeParticipant,
|
||||
actorId: 'chairman1',
|
||||
targetId: 'payer1',
|
||||
payload: { reason: 'x' },
|
||||
status: CriticalActionStatus.Pending,
|
||||
confirmations: [{ by: 'chairman1', at: '2026-06-14T00:00:00Z' }],
|
||||
createdAt: '2026-06-14T00:00:00Z',
|
||||
expiresAt: '2026-06-15T00:00:00Z',
|
||||
finalizedAt: null,
|
||||
};
|
||||
|
||||
function build() {
|
||||
const criticalActions = {
|
||||
initiate: jest.fn(async () => PENDING),
|
||||
confirm: jest.fn(async () => ({ ...PENDING, status: CriticalActionStatus.Confirmed })),
|
||||
getAuditTrail: jest.fn(async () => [
|
||||
{
|
||||
id: 'ca1',
|
||||
actionType: CriticalActionType.ExcludeParticipant,
|
||||
targetId: 'payer1',
|
||||
status: CriticalActionStatus.Confirmed,
|
||||
createdAt: '2026-06-14T00:00:00Z',
|
||||
finalizedAt: '2026-06-14T01:00:00Z',
|
||||
initiatorId: 'chairman1',
|
||||
initiatedAt: '2026-06-14T00:00:00Z',
|
||||
confirmerIds: [{ by: 'member1', at: '2026-06-14T00:30:00Z' }],
|
||||
payloadHash: 'deadbeef',
|
||||
},
|
||||
]),
|
||||
};
|
||||
const keyRevocation = {
|
||||
revoke: jest.fn(async () => ({ status: 'revoked', targetId: 'payer1', sessionsRevoked: 2, mustRecover: true })),
|
||||
};
|
||||
const forceRecovery = {
|
||||
requestConsent: jest.fn(async () => undefined),
|
||||
authorize: jest.fn(async () => ({
|
||||
authorized: true,
|
||||
consentVia: ForceRecoveryConsentVia.AssemblyDecision,
|
||||
triggeredBy: 'chairman',
|
||||
})),
|
||||
};
|
||||
const resolver = new CriticalActionsResolver(
|
||||
criticalActions as unknown as CriticalActionsService,
|
||||
keyRevocation as unknown as KeyRevocationService,
|
||||
forceRecovery as unknown as ForceRecoveryService,
|
||||
);
|
||||
return { resolver, criticalActions, keyRevocation, forceRecovery };
|
||||
}
|
||||
|
||||
describe('CriticalActionsResolver', () => {
|
||||
it('initiateCriticalAction ставит actorId = текущий пользователь и маппит pending в snake_case', async () => {
|
||||
const { resolver, criticalActions } = build();
|
||||
const out = await resolver.initiateCriticalAction(
|
||||
{ action_type: CriticalActionType.ExcludeParticipant, target_id: 'payer1', payload: { reason: 'x' } },
|
||||
CHAIRMAN,
|
||||
);
|
||||
expect(criticalActions.initiate).toHaveBeenCalledWith({
|
||||
actionType: CriticalActionType.ExcludeParticipant,
|
||||
actorId: 'chairman1',
|
||||
targetId: 'payer1',
|
||||
payload: { reason: 'x' },
|
||||
});
|
||||
expect(out).toEqual({
|
||||
id: 'ca1',
|
||||
action_type: CriticalActionType.ExcludeParticipant,
|
||||
actor_id: 'chairman1',
|
||||
target_id: 'payer1',
|
||||
payload: { reason: 'x' },
|
||||
status: CriticalActionStatus.Pending,
|
||||
confirmations: [{ by: 'chairman1', at: '2026-06-14T00:00:00Z' }],
|
||||
created_at: '2026-06-14T00:00:00Z',
|
||||
expires_at: '2026-06-15T00:00:00Z',
|
||||
finalized_at: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('initiateCriticalAction без payload передаёт undefined', async () => {
|
||||
const { resolver, criticalActions } = build();
|
||||
await resolver.initiateCriticalAction(
|
||||
{ action_type: CriticalActionType.ForceRecovery, target_id: 'payer1' },
|
||||
CHAIRMAN,
|
||||
);
|
||||
expect(criticalActions.initiate).toHaveBeenCalledWith({
|
||||
actionType: CriticalActionType.ForceRecovery,
|
||||
actorId: 'chairman1',
|
||||
targetId: 'payer1',
|
||||
payload: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('confirmCriticalAction подтверждает от имени текущего пользователя', async () => {
|
||||
const { resolver, criticalActions } = build();
|
||||
const out = await resolver.confirmCriticalAction('ca1', CHAIRMAN);
|
||||
expect(criticalActions.confirm).toHaveBeenCalledWith('ca1', 'chairman1');
|
||||
expect(out.status).toBe(CriticalActionStatus.Confirmed);
|
||||
});
|
||||
|
||||
it('getCriticalActionAuditTrail маппит атрибуцию в snake_case', async () => {
|
||||
const { resolver, criticalActions } = build();
|
||||
const out = await resolver.getCriticalActionAuditTrail('payer1');
|
||||
expect(criticalActions.getAuditTrail).toHaveBeenCalledWith('payer1');
|
||||
expect(out).toEqual([
|
||||
{
|
||||
id: 'ca1',
|
||||
action_type: CriticalActionType.ExcludeParticipant,
|
||||
target_id: 'payer1',
|
||||
status: CriticalActionStatus.Confirmed,
|
||||
created_at: '2026-06-14T00:00:00Z',
|
||||
finalized_at: '2026-06-14T01:00:00Z',
|
||||
initiator_id: 'chairman1',
|
||||
initiated_at: '2026-06-14T00:00:00Z',
|
||||
confirmer_ids: [{ by: 'member1', at: '2026-06-14T00:30:00Z' }],
|
||||
payload_hash: 'deadbeef',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('revokeParticipantKey ставит chairmanId = текущий пользователь и маппит результат', async () => {
|
||||
const { resolver, keyRevocation } = build();
|
||||
const out = await resolver.revokeParticipantKey({ target_id: 'payer1', reason: 'compromise' }, CHAIRMAN, IP);
|
||||
expect(keyRevocation.revoke).toHaveBeenCalledWith({
|
||||
targetId: 'payer1',
|
||||
reason: 'compromise',
|
||||
chairmanId: 'chairman1',
|
||||
ip: IP,
|
||||
});
|
||||
expect(out).toEqual({ status: 'revoked', target_id: 'payer1', sessions_revoked: 2, must_recover: true });
|
||||
});
|
||||
|
||||
it('requestForceRecoveryConsent зовёт сервис с инициатором и возвращает true', async () => {
|
||||
const { resolver, forceRecovery } = build();
|
||||
const ok = await resolver.requestForceRecoveryConsent({ target_id: 'payer1' }, CHAIRMAN, IP);
|
||||
expect(ok).toBe(true);
|
||||
expect(forceRecovery.requestConsent).toHaveBeenCalledWith('payer1', 'chairman1', IP);
|
||||
});
|
||||
|
||||
it('authorizeForceRecovery маппит результат и прокидывает основания', async () => {
|
||||
const { resolver, forceRecovery } = build();
|
||||
const out = await resolver.authorizeForceRecovery(
|
||||
{ target_id: 'payer1', assembly_decision_tx_id: 'tx9', critical_action_id: 'ca1' },
|
||||
CHAIRMAN,
|
||||
IP,
|
||||
);
|
||||
expect(forceRecovery.authorize).toHaveBeenCalledWith({
|
||||
targetId: 'payer1',
|
||||
initiatorId: 'chairman1',
|
||||
assemblyDecisionTxId: 'tx9',
|
||||
criticalActionId: 'ca1',
|
||||
ip: IP,
|
||||
});
|
||||
expect(out).toEqual({
|
||||
authorized: true,
|
||||
consent_via: ForceRecoveryConsentVia.AssemblyDecision,
|
||||
triggered_by: 'chairman',
|
||||
});
|
||||
});
|
||||
|
||||
it('authorizeForceRecovery без необязательных полей передаёт undefined', async () => {
|
||||
const { resolver, forceRecovery } = build();
|
||||
await resolver.authorizeForceRecovery({ target_id: 'payer1' }, CHAIRMAN, IP);
|
||||
expect(forceRecovery.authorize).toHaveBeenCalledWith({
|
||||
targetId: 'payer1',
|
||||
initiatorId: 'chairman1',
|
||||
assemblyDecisionTxId: undefined,
|
||||
criticalActionId: undefined,
|
||||
ip: IP,
|
||||
});
|
||||
});
|
||||
});
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
|
||||
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
|
||||
import { ClientIp } from '~/application/auth/decorators/request-meta.decorator';
|
||||
import type { PendingCriticalAction } from '~/domain/auth-v2/ports/pending-critical-actions.port';
|
||||
import { AuthorizationGuard } from '../authorization/authorization.guard';
|
||||
import { CheckAbility } from '../authorization/check-ability.decorator';
|
||||
import { CriticalActionsService, type CriticalActionAuditEntry } from './critical-actions.service';
|
||||
import { KeyRevocationService } from '../key-revocation/key-revocation.service';
|
||||
import { ForceRecoveryService } from '../force-recovery/force-recovery.service';
|
||||
import {
|
||||
AuthorizeForceRecoveryInputDTO,
|
||||
CriticalActionAuditEntryDTO,
|
||||
ForceRecoveryAuthorizationDTO,
|
||||
InitiateCriticalActionInputDTO,
|
||||
PendingCriticalActionDTO,
|
||||
RequestForceRecoveryConsentInputDTO,
|
||||
RevokeKeyResultDTO,
|
||||
RevokeParticipantKeyInputDTO,
|
||||
} from './dto/critical-actions.dto';
|
||||
|
||||
interface ICurrentUser {
|
||||
id: string;
|
||||
username: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
function mapPending(p: PendingCriticalAction): PendingCriticalActionDTO {
|
||||
return {
|
||||
id: p.id,
|
||||
action_type: p.actionType,
|
||||
actor_id: p.actorId,
|
||||
target_id: p.targetId,
|
||||
payload: p.payload,
|
||||
status: p.status,
|
||||
confirmations: p.confirmations.map((c) => ({ by: c.by, at: c.at })),
|
||||
created_at: p.createdAt,
|
||||
expires_at: p.expiresAt,
|
||||
finalized_at: p.finalizedAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL-фасад критических действий совета (Фаза 2 миграции REST→GraphQL/SDK).
|
||||
* Заменяет REST-контроллеры `coop/critical-actions`, `coop/keys` и JWT-методы
|
||||
* `coop/force-recovery` (request-consent/authorize) — фронт ходит через
|
||||
* @coopenomics/sdk (Zeus), нового способа взаимодействия с бэкендом наружу не появляется.
|
||||
*
|
||||
* Авторизация — тот же субстрат Эпика 6: `@CheckAbility` + `AuthorizationGuard`
|
||||
* (уже GraphQL-aware), те же способности, что были на REST. Magic-link
|
||||
* `coop/force-recovery/consent/:token` остаётся REST (клик из письма без SDK-контекста).
|
||||
*/
|
||||
@Resolver()
|
||||
export class CriticalActionsResolver {
|
||||
constructor(
|
||||
private readonly criticalActions: CriticalActionsService,
|
||||
private readonly keyRevocation: KeyRevocationService,
|
||||
private readonly forceRecovery: ForceRecoveryService,
|
||||
) {}
|
||||
|
||||
@Mutation(() => PendingCriticalActionDTO, {
|
||||
name: 'initiateCriticalAction',
|
||||
description: 'Инициировать критическое действие совета (председатель)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, AuthorizationGuard)
|
||||
@CheckAbility('create', 'CriticalAction')
|
||||
async initiateCriticalAction(
|
||||
@Args('data', { type: () => InitiateCriticalActionInputDTO }) data: InitiateCriticalActionInputDTO,
|
||||
@CurrentUser() user: ICurrentUser,
|
||||
): Promise<PendingCriticalActionDTO> {
|
||||
const action = await this.criticalActions.initiate({
|
||||
actionType: data.action_type,
|
||||
actorId: user.username,
|
||||
targetId: data.target_id,
|
||||
payload: data.payload ?? undefined,
|
||||
});
|
||||
return mapPending(action);
|
||||
}
|
||||
|
||||
@Mutation(() => PendingCriticalActionDTO, {
|
||||
name: 'confirmCriticalAction',
|
||||
description: 'Подтвердить критическое действие совета (член совета)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, AuthorizationGuard)
|
||||
@CheckAbility('confirm', 'CriticalAction')
|
||||
async confirmCriticalAction(
|
||||
@Args('id', { type: () => String }) id: string,
|
||||
@CurrentUser() user: ICurrentUser,
|
||||
): Promise<PendingCriticalActionDTO> {
|
||||
const action = await this.criticalActions.confirm(id, user.username);
|
||||
return mapPending(action);
|
||||
}
|
||||
|
||||
@Query(() => [CriticalActionAuditEntryDTO], {
|
||||
name: 'getCriticalActionAuditTrail',
|
||||
description: 'Audit-trail критических действий, затрагивающих пайщика (для контролирующего органа)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, AuthorizationGuard)
|
||||
@CheckAbility('read', 'CriticalAction')
|
||||
async getCriticalActionAuditTrail(
|
||||
@Args('target_id', { type: () => String }) targetId: string,
|
||||
): Promise<CriticalActionAuditEntryDTO[]> {
|
||||
const entries: CriticalActionAuditEntry[] = await this.criticalActions.getAuditTrail(targetId);
|
||||
return entries.map((e) => ({
|
||||
id: e.id,
|
||||
action_type: e.actionType,
|
||||
target_id: e.targetId,
|
||||
status: e.status,
|
||||
created_at: e.createdAt,
|
||||
finalized_at: e.finalizedAt,
|
||||
initiator_id: e.initiatorId,
|
||||
initiated_at: e.initiatedAt,
|
||||
confirmer_ids: e.confirmerIds.map((c) => ({ by: c.by, at: c.at })),
|
||||
payload_hash: e.payloadHash,
|
||||
}));
|
||||
}
|
||||
|
||||
@Mutation(() => RevokeKeyResultDTO, {
|
||||
name: 'revokeParticipantKey',
|
||||
description: 'Отозвать скомпрометированный ключ пайщика (председатель)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, AuthorizationGuard)
|
||||
@CheckAbility('update', 'Participant')
|
||||
async revokeParticipantKey(
|
||||
@Args('data', { type: () => RevokeParticipantKeyInputDTO }) data: RevokeParticipantKeyInputDTO,
|
||||
@CurrentUser() user: ICurrentUser,
|
||||
@ClientIp() ip: string | null,
|
||||
): Promise<RevokeKeyResultDTO> {
|
||||
const result = await this.keyRevocation.revoke({
|
||||
targetId: data.target_id,
|
||||
reason: data.reason,
|
||||
chairmanId: user.username,
|
||||
ip,
|
||||
});
|
||||
return {
|
||||
status: result.status,
|
||||
target_id: result.targetId,
|
||||
sessions_revoked: result.sessionsRevoked,
|
||||
must_recover: result.mustRecover,
|
||||
};
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
name: 'requestForceRecoveryConsent',
|
||||
description: 'Запросить согласие пайщика на принудительное восстановление (председатель)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, AuthorizationGuard)
|
||||
@CheckAbility('create', 'CriticalAction')
|
||||
async requestForceRecoveryConsent(
|
||||
@Args('data', { type: () => RequestForceRecoveryConsentInputDTO }) data: RequestForceRecoveryConsentInputDTO,
|
||||
@CurrentUser() user: ICurrentUser,
|
||||
@ClientIp() ip: string | null,
|
||||
): Promise<boolean> {
|
||||
await this.forceRecovery.requestConsent(data.target_id, user.username, ip);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => ForceRecoveryAuthorizationDTO, {
|
||||
name: 'authorizeForceRecovery',
|
||||
description: 'Авторизовать принудительное восстановление доступа пайщика (председатель)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, AuthorizationGuard)
|
||||
@CheckAbility('create', 'CriticalAction')
|
||||
async authorizeForceRecovery(
|
||||
@Args('data', { type: () => AuthorizeForceRecoveryInputDTO }) data: AuthorizeForceRecoveryInputDTO,
|
||||
@CurrentUser() user: ICurrentUser,
|
||||
@ClientIp() ip: string | null,
|
||||
): Promise<ForceRecoveryAuthorizationDTO> {
|
||||
const auth = await this.forceRecovery.authorize({
|
||||
targetId: data.target_id,
|
||||
initiatorId: user.username,
|
||||
assemblyDecisionTxId: data.assembly_decision_tx_id ?? undefined,
|
||||
criticalActionId: data.critical_action_id ?? undefined,
|
||||
ip,
|
||||
});
|
||||
return {
|
||||
authorized: auth.authorized,
|
||||
consent_via: auth.consentVia,
|
||||
triggered_by: auth.triggeredBy,
|
||||
};
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import { Field, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
import { CriticalActionStatus, CriticalActionType } from '~/domain/auth-v2/ports/pending-critical-actions.port';
|
||||
import { ForceRecoveryConsentVia } from '../../force-recovery/force-recovery.service';
|
||||
|
||||
/**
|
||||
* GraphQL-контракт критических действий совета (Фаза 2 миграции REST→GraphQL/SDK):
|
||||
* multi-party critical actions (Story 6.8), audit-trail (6.10), manual key-revoke (4.7),
|
||||
* force-recovery председателем (6.9). Зеркало доменных типов auth-v2, поля — snake_case
|
||||
* по канону GraphQL этого репозитория; резолвер маппит camelCase сервисов в snake_case.
|
||||
*
|
||||
* Зачем GraphQL, а не REST: единый типизированный фасад фронта — @coopenomics/sdk
|
||||
* (Zeus). Авторизация — тот же субстрат Эпика 6 (`@CheckAbility` + `AuthorizationGuard`,
|
||||
* уже GraphQL-aware). Magic-link `:token`-эндпоинты (согласие пайщика, one-click из
|
||||
* письма) остаются REST — клик без SDK-контекста.
|
||||
*/
|
||||
|
||||
registerEnumType(CriticalActionType, {
|
||||
name: 'CriticalActionType',
|
||||
description: 'Тип критического действия совета',
|
||||
});
|
||||
|
||||
registerEnumType(CriticalActionStatus, {
|
||||
name: 'CriticalActionStatus',
|
||||
description: 'Состояние критического действия',
|
||||
});
|
||||
|
||||
registerEnumType(ForceRecoveryConsentVia, {
|
||||
name: 'ForceRecoveryConsentVia',
|
||||
description: 'Чем подтверждено принудительное восстановление: согласием пайщика или решением собрания',
|
||||
});
|
||||
|
||||
/** Одно подтверждение критического действия: кто и когда. */
|
||||
@ObjectType('CriticalActionConfirmation')
|
||||
export class CriticalActionConfirmationDTO {
|
||||
@Field(() => String, { description: 'Кто подтвердил (имя аккаунта)' })
|
||||
by!: string;
|
||||
|
||||
@Field(() => String, { description: 'Когда подтвердил (ISO)' })
|
||||
at!: string;
|
||||
}
|
||||
|
||||
/** Pending-запись критического действия (ждёт кворума подтверждений в окне ≤24ч). */
|
||||
@ObjectType('PendingCriticalAction')
|
||||
export class PendingCriticalActionDTO {
|
||||
@Field(() => String, { description: 'Идентификатор критического действия' })
|
||||
id!: string;
|
||||
|
||||
@Field(() => CriticalActionType, { description: 'Тип действия' })
|
||||
action_type!: CriticalActionType;
|
||||
|
||||
@Field(() => String, { description: 'Инициатор (председатель)' })
|
||||
actor_id!: string;
|
||||
|
||||
@Field(() => String, { description: 'Кого/что затрагивает действие' })
|
||||
target_id!: string;
|
||||
|
||||
@Field(() => GraphQLJSON, { description: 'Содержимое действия (зависит от типа)' })
|
||||
payload!: Record<string, unknown>;
|
||||
|
||||
@Field(() => CriticalActionStatus, { description: 'Состояние действия' })
|
||||
status!: CriticalActionStatus;
|
||||
|
||||
@Field(() => [CriticalActionConfirmationDTO], { description: 'Накопленные подтверждения' })
|
||||
confirmations!: CriticalActionConfirmationDTO[];
|
||||
|
||||
@Field(() => String, { description: 'Момент инициации (ISO)' })
|
||||
created_at!: string;
|
||||
|
||||
@Field(() => String, { description: 'Крайний срок подтверждения (ISO)' })
|
||||
expires_at!: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Момент финализации (ISO); пусто, пока не финализировано' })
|
||||
finalized_at!: string | null;
|
||||
}
|
||||
|
||||
/** Запись audit-trail критического действия: полная атрибуция для контролирующего органа. */
|
||||
@ObjectType('CriticalActionAuditEntry')
|
||||
export class CriticalActionAuditEntryDTO {
|
||||
@Field(() => String, { description: 'Идентификатор критического действия' })
|
||||
id!: string;
|
||||
|
||||
@Field(() => CriticalActionType, { description: 'Тип действия' })
|
||||
action_type!: CriticalActionType;
|
||||
|
||||
@Field(() => String, { description: 'Кого/что затрагивает действие' })
|
||||
target_id!: string;
|
||||
|
||||
@Field(() => CriticalActionStatus, { description: 'Состояние действия' })
|
||||
status!: CriticalActionStatus;
|
||||
|
||||
@Field(() => String, { description: 'Момент инициации (ISO)' })
|
||||
created_at!: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Момент финализации (ISO); пусто, пока не финализировано' })
|
||||
finalized_at!: string | null;
|
||||
|
||||
@Field(() => String, { description: 'Инициатор (председатель)' })
|
||||
initiator_id!: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Момент первой подписи инициатора (ISO)' })
|
||||
initiated_at!: string | null;
|
||||
|
||||
@Field(() => [CriticalActionConfirmationDTO], { description: 'Подтверждающие совета (≠ инициатор) со своими timestamp' })
|
||||
confirmer_ids!: CriticalActionConfirmationDTO[];
|
||||
|
||||
@Field(() => String, { description: 'SHA-256 от payload — гарантия неподменяемости содержимого' })
|
||||
payload_hash!: string;
|
||||
}
|
||||
|
||||
/** Результат отзыва ключа пайщика председателем. */
|
||||
@ObjectType('RevokeKeyResult')
|
||||
export class RevokeKeyResultDTO {
|
||||
@Field(() => String, { description: 'Статус операции' })
|
||||
status!: string;
|
||||
|
||||
@Field(() => String, { description: 'Пайщик, чей ключ отозван' })
|
||||
target_id!: string;
|
||||
|
||||
@Field(() => Int, { description: 'Сколько активных сессий пайщика отозвано' })
|
||||
sessions_revoked!: number;
|
||||
|
||||
@Field(() => Boolean, { description: 'Пайщик обязан пройти recovery для получения нового ключа' })
|
||||
must_recover!: boolean;
|
||||
}
|
||||
|
||||
/** Результат авторизации принудительного восстановления председателем. */
|
||||
@ObjectType('ForceRecoveryAuthorization')
|
||||
export class ForceRecoveryAuthorizationDTO {
|
||||
@Field(() => Boolean, { description: 'Восстановление авторизовано' })
|
||||
authorized!: boolean;
|
||||
|
||||
@Field(() => ForceRecoveryConsentVia, { description: 'Чем подтверждено восстановление' })
|
||||
consent_via!: ForceRecoveryConsentVia;
|
||||
|
||||
@Field(() => String, { description: 'Кто инициировал (председатель)' })
|
||||
triggered_by!: string;
|
||||
}
|
||||
|
||||
/** Вход на инициацию критического действия. */
|
||||
@InputType('InitiateCriticalActionInput')
|
||||
export class InitiateCriticalActionInputDTO {
|
||||
@Field(() => CriticalActionType, { description: 'Тип критического действия' })
|
||||
action_type!: CriticalActionType;
|
||||
|
||||
@Field(() => String, { description: 'Кого/что затрагивает действие' })
|
||||
target_id!: string;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true, description: 'Содержимое действия (зависит от типа)' })
|
||||
payload?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
/** Вход на отзыв ключа пайщика. */
|
||||
@InputType('RevokeParticipantKeyInput')
|
||||
export class RevokeParticipantKeyInputDTO {
|
||||
@Field(() => String, { description: 'Пайщик, чей ключ отзывается' })
|
||||
target_id!: string;
|
||||
|
||||
@Field(() => String, { description: 'Обоснование отзыва' })
|
||||
reason!: string;
|
||||
}
|
||||
|
||||
/** Вход на запрос согласия пайщика на принудительное восстановление. */
|
||||
@InputType('RequestForceRecoveryConsentInput')
|
||||
export class RequestForceRecoveryConsentInputDTO {
|
||||
@Field(() => String, { description: 'Пайщик, для которого запрашивается согласие' })
|
||||
target_id!: string;
|
||||
}
|
||||
|
||||
/** Вход на авторизацию принудительного восстановления председателем. */
|
||||
@InputType('AuthorizeForceRecoveryInput')
|
||||
export class AuthorizeForceRecoveryInputDTO {
|
||||
@Field(() => String, { description: 'Пайщик, для которого авторизуется восстановление' })
|
||||
target_id!: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Идентификатор транзакции решения собрания (если основание — собрание)' })
|
||||
assembly_decision_tx_id?: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Идентификатор связанного критического действия' })
|
||||
critical_action_id?: string | null;
|
||||
}
|
||||
+10
-43
@@ -1,58 +1,25 @@
|
||||
import { BadRequestException, Body, Controller, Param, Post, Req, UnauthorizedException, UseFilters, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Param, Post, Req, UseFilters } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard';
|
||||
import { AuthorizationGuard } from '../authorization/authorization.guard';
|
||||
import { CheckAbility } from '../authorization/check-ability.decorator';
|
||||
import { AuthV2ExceptionFilter } from '../exceptions/auth-v2-exception.filter';
|
||||
import { ForceRecoveryService, type ForceRecoveryAuthorization } from './force-recovery.service';
|
||||
|
||||
interface AuthedRequest extends Request {
|
||||
user?: { id: string; username: string; role?: string };
|
||||
}
|
||||
import { ForceRecoveryService } from './force-recovery.service';
|
||||
|
||||
/**
|
||||
* Force-recovery (CoopID, Story 6.9). Председатель (capability `create CriticalAction`)
|
||||
* запрашивает согласие пайщика и авторизует сброс; пайщик подтверждает согласие по
|
||||
* magic-link без auth-guard (доступ мог быть утрачен). Гейт — в сервисе.
|
||||
* Согласие пайщика на принудительное восстановление по magic-link (CoopID, Story 6.9).
|
||||
* Остаётся REST: пайщик подтверждает согласие кликом из письма без auth-guard и без
|
||||
* SDK-контекста (доступ мог быть утрачен), авторизация — по одноразовому токену из ссылки.
|
||||
*
|
||||
* Запрос согласия и авторизация председателем (под JWT+CASL) переведены в GraphQL/SDK —
|
||||
* см. `CriticalActionsResolver.requestForceRecoveryConsent` / `authorizeForceRecovery`
|
||||
* (Фаза 2 миграции REST→GraphQL/SDK).
|
||||
*/
|
||||
@Controller('coop/force-recovery')
|
||||
@UseFilters(AuthV2ExceptionFilter)
|
||||
export class ForceRecoveryController {
|
||||
constructor(private readonly service: ForceRecoveryService) {}
|
||||
|
||||
@Post('request-consent')
|
||||
@UseGuards(HttpJwtAuthGuard, AuthorizationGuard)
|
||||
@CheckAbility('create', 'CriticalAction')
|
||||
async requestConsent(@Body() body: { targetId: string }, @Req() req: AuthedRequest): Promise<{ status: 'requested' }> {
|
||||
const initiatorId = req.user?.username;
|
||||
if (!initiatorId) throw new UnauthorizedException();
|
||||
if (!body?.targetId) throw new BadRequestException('targetId обязателен');
|
||||
await this.service.requestConsent(body.targetId, initiatorId, req.ip ?? null);
|
||||
return { status: 'requested' };
|
||||
}
|
||||
|
||||
@Post('consent/:token')
|
||||
async grantConsent(@Param('token') token: string, @Req() req: AuthedRequest): Promise<{ status: 'granted'; targetId: string }> {
|
||||
async grantConsent(@Param('token') token: string, @Req() req: Request): Promise<{ status: 'granted'; targetId: string }> {
|
||||
const { targetId } = await this.service.grantConsent(token, req.ip ?? null);
|
||||
return { status: 'granted', targetId };
|
||||
}
|
||||
|
||||
@Post('authorize')
|
||||
@UseGuards(HttpJwtAuthGuard, AuthorizationGuard)
|
||||
@CheckAbility('create', 'CriticalAction')
|
||||
async authorize(
|
||||
@Body() body: { targetId: string; assemblyDecisionTxId?: string; criticalActionId?: string },
|
||||
@Req() req: AuthedRequest,
|
||||
): Promise<ForceRecoveryAuthorization> {
|
||||
const initiatorId = req.user?.username;
|
||||
if (!initiatorId) throw new UnauthorizedException();
|
||||
if (!body?.targetId) throw new BadRequestException('targetId обязателен');
|
||||
return this.service.authorize({
|
||||
targetId: body.targetId,
|
||||
initiatorId,
|
||||
assemblyDecisionTxId: body.assemblyDecisionTxId,
|
||||
criticalActionId: body.criticalActionId,
|
||||
ip: req.ip ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
import { BadRequestException, Body, Controller, Post, Req, UnauthorizedException, UseFilters, UseGuards } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard';
|
||||
import { AuthorizationGuard } from '../authorization/authorization.guard';
|
||||
import { CheckAbility } from '../authorization/check-ability.decorator';
|
||||
import { AuthV2ExceptionFilter } from '../exceptions/auth-v2-exception.filter';
|
||||
import { KeyRevocationService, type RevokeKeyResult } from './key-revocation.service';
|
||||
|
||||
interface AuthedRequest extends Request {
|
||||
user?: { id: string; username: string; role?: string };
|
||||
}
|
||||
|
||||
interface RevokeBody {
|
||||
targetId: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual revoke (CoopID, Story 4.7). Председатель отзывает скомпрометированный ключ
|
||||
* пайщика. Право — `update Participant` (председатель администрирует security-состояние
|
||||
* пайщика; отдельной CASL-способности `revoke` не вводим — матрица не расширяется).
|
||||
* Сессии, pending-state и аудит — в сервисе.
|
||||
*/
|
||||
@Controller('coop/keys')
|
||||
@UseFilters(AuthV2ExceptionFilter)
|
||||
export class KeyRevocationController {
|
||||
constructor(private readonly service: KeyRevocationService) {}
|
||||
|
||||
@Post('revoke')
|
||||
@UseGuards(HttpJwtAuthGuard, AuthorizationGuard)
|
||||
@CheckAbility('update', 'Participant')
|
||||
async revoke(@Body() body: RevokeBody, @Req() req: AuthedRequest): Promise<RevokeKeyResult> {
|
||||
const chairmanId = req.user?.username;
|
||||
if (!chairmanId) throw new UnauthorizedException();
|
||||
if (!body?.targetId) throw new BadRequestException('targetId обязателен');
|
||||
if (!body?.reason) throw new BadRequestException('reason обязателен (обоснование отзыва)');
|
||||
return this.service.revoke({ targetId: body.targetId, reason: body.reason, chairmanId, ip: req.ip ?? null });
|
||||
}
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
Post,
|
||||
Req,
|
||||
UnauthorizedException,
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard';
|
||||
import { isRecoveryStrategy, RecoveryStrategy } from '~/domain/auth-v2/recovery-strategy/recovery-strategy.types';
|
||||
import { AuthV2ExceptionFilter } from '../exceptions/auth-v2-exception.filter';
|
||||
import { RecoveryStrategyService } from './recovery-strategy.service';
|
||||
|
||||
interface AuthedRequest extends Request {
|
||||
user?: { id: string; username: string };
|
||||
}
|
||||
|
||||
interface SetStrategyBody {
|
||||
strategy?: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка стратегии восстановления (CoopID, Story 3.5). Под `HttpJwtAuthGuard` —
|
||||
* для текущего залогиненного пайщика (subject = `user.id`). Смена требует step-up
|
||||
* второго фактора (TOTP) — см. отступление от AC в спеке.
|
||||
*/
|
||||
@Controller('coop/recovery/strategy')
|
||||
@UseFilters(AuthV2ExceptionFilter)
|
||||
@UseGuards(HttpJwtAuthGuard)
|
||||
export class RecoveryStrategyController {
|
||||
constructor(private readonly strategy: RecoveryStrategyService) {}
|
||||
|
||||
/** Текущая стратегия пайщика. */
|
||||
@Get()
|
||||
async get(@Req() req: AuthedRequest): Promise<{ strategy: RecoveryStrategy }> {
|
||||
const user = req.user;
|
||||
if (!user?.id) throw new UnauthorizedException();
|
||||
return { strategy: await this.strategy.getStrategy(user.id) };
|
||||
}
|
||||
|
||||
/** Сменить стратегию (требует TOTP-код step-up). */
|
||||
@Post()
|
||||
@HttpCode(204)
|
||||
async set(@Body() body: SetStrategyBody, @Req() req: AuthedRequest): Promise<void> {
|
||||
const user = req.user;
|
||||
if (!user?.id) throw new UnauthorizedException();
|
||||
if (!isRecoveryStrategy(body?.strategy)) throw new BadRequestException('Недопустимая стратегия восстановления');
|
||||
if (!body?.code || typeof body.code !== 'string') throw new BadRequestException('Требуется code');
|
||||
await this.strategy.setStrategy(user.id, body.strategy, body.code, req.ip ?? null);
|
||||
}
|
||||
}
|
||||
+8
-25
@@ -1,41 +1,24 @@
|
||||
import { Body, Controller, Param, Post, Req, UnauthorizedException, UseFilters, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Param, Post, Req, UseFilters } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard';
|
||||
import { AuthV2ExceptionFilter } from '../exceptions/auth-v2-exception.filter';
|
||||
import { SecurityIncidentService } from './security-incident.service';
|
||||
|
||||
interface AuthedRequest extends Request {
|
||||
user?: { id: string; username: string };
|
||||
}
|
||||
|
||||
interface NotMeBody {
|
||||
/** id сессии, помеченной подозрительной (опц., из настроек). */
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Флаг «Это не я» (CoopID, Story 3.10) — немедленный отзыв всех сессий пайщика.
|
||||
* One-click «Это не я» из письма о новом устройстве (CoopID, Story 3.9) — немедленный
|
||||
* отзыв всех сессий пайщика. Остаётся REST: клик по magic-link из письма приходит без
|
||||
* аутентификации и без SDK-контекста (своя сессия могла быть скомпрометирована),
|
||||
* авторизация — по одноразовому токену из ссылки.
|
||||
*
|
||||
* `POST /coop/security/not-me` — из настроек ЛК (под JWT-guard, subject = user.id).
|
||||
* `POST /coop/security/not-me/:token` — one-click из письма о новом устройстве (3.9):
|
||||
* без аутентификации (своя сессия могла быть скомпрометирована), авторизация — по
|
||||
* одноразовому токену из ссылки.
|
||||
* Сигнал «Это не я» из настроек ЛК (под JWT) переведён в GraphQL/SDK —
|
||||
* см. `AccountSecurityResolver.reportNotMe` (Фаза 2 миграции REST→GraphQL/SDK).
|
||||
*/
|
||||
@Controller('coop/security')
|
||||
@UseFilters(AuthV2ExceptionFilter)
|
||||
export class SecurityIncidentController {
|
||||
constructor(private readonly incidents: SecurityIncidentService) {}
|
||||
|
||||
@Post('not-me')
|
||||
@UseGuards(HttpJwtAuthGuard)
|
||||
async notMe(@Body() body: NotMeBody, @Req() req: AuthedRequest): Promise<{ revoked: number }> {
|
||||
const user = req.user;
|
||||
if (!user?.id) throw new UnauthorizedException();
|
||||
return this.incidents.report({ subjectId: user.id, ip: req.ip ?? null, source: 'settings', reportedSessionId: body?.sessionId ?? null });
|
||||
}
|
||||
|
||||
@Post('not-me/:token')
|
||||
async notMeOneClick(@Param('token') token: string, @Req() req: AuthedRequest): Promise<{ revoked: number }> {
|
||||
async notMeOneClick(@Param('token') token: string, @Req() req: Request): Promise<{ revoked: number }> {
|
||||
return this.incidents.reportByToken(token, req.ip ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Controller, Delete, Get, HttpCode, Param, Req, UnauthorizedException, UseFilters, UseGuards } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard';
|
||||
import type { ActiveSession } from '~/domain/auth-v2/sessions/session.types';
|
||||
import { AuthV2ExceptionFilter } from '../exceptions/auth-v2-exception.filter';
|
||||
import { SessionsService } from './sessions.service';
|
||||
|
||||
interface AuthedRequest extends Request {
|
||||
user?: { id: string; username: string };
|
||||
}
|
||||
|
||||
/** Заголовок, которым SPA передаёт свой refresh-токен, чтобы пометить текущую сессию в списке. */
|
||||
const CURRENT_SESSION_HEADER = 'x-coop-refresh-token';
|
||||
|
||||
/**
|
||||
* Активные сессии пайщика (Story 3.7) под `HttpJwtAuthGuard` — subject = `user.id`.
|
||||
* GET — список устройств; DELETE /:id — отозвать одну; DELETE — отозвать все.
|
||||
*/
|
||||
@Controller('coop/sessions')
|
||||
@UseFilters(AuthV2ExceptionFilter)
|
||||
@UseGuards(HttpJwtAuthGuard)
|
||||
export class SessionsController {
|
||||
constructor(private readonly sessions: SessionsService) {}
|
||||
|
||||
/** Список активных сессий; текущая помечается, если передан X-Coop-Refresh-Token. */
|
||||
@Get()
|
||||
async list(@Req() req: AuthedRequest): Promise<{ sessions: ActiveSession[] }> {
|
||||
const user = req.user;
|
||||
if (!user?.id) throw new UnauthorizedException();
|
||||
const current = req.header(CURRENT_SESSION_HEADER) ?? null;
|
||||
return { sessions: await this.sessions.list(user.id, current) };
|
||||
}
|
||||
|
||||
/** Завершить конкретную сессию. */
|
||||
@Delete(':id')
|
||||
@HttpCode(204)
|
||||
async revoke(@Param('id') id: string, @Req() req: AuthedRequest): Promise<void> {
|
||||
const user = req.user;
|
||||
if (!user?.id) throw new UnauthorizedException();
|
||||
await this.sessions.revoke(user.id, id, req.ip ?? null);
|
||||
}
|
||||
|
||||
/** Завершить все сессии пайщика. */
|
||||
@Delete()
|
||||
async revokeAll(@Req() req: AuthedRequest): Promise<{ revoked: number }> {
|
||||
const user = req.user;
|
||||
if (!user?.id) throw new UnauthorizedException();
|
||||
return this.sessions.revokeAll(user.id, req.ip ?? null);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
Post,
|
||||
Req,
|
||||
UnauthorizedException,
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { HttpJwtAuthGuard } from '~/application/auth/guards/http-jwt-auth.guard';
|
||||
import { AuthV2ExceptionFilter } from '../exceptions/auth-v2-exception.filter';
|
||||
import { TwoFactorService } from './two-factor.service';
|
||||
import type { EnrollmentChallenge } from './two-factor.service';
|
||||
|
||||
interface AuthedRequest extends Request {
|
||||
user?: { id: string; username: string };
|
||||
}
|
||||
|
||||
interface CodeBody {
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Управление вторым фактором (TOTP / Google Authenticator) — CoopID Story 3.6.
|
||||
* Все операции — для текущего залогиненного пайщика (HttpJwtAuthGuard → req.user).
|
||||
* Subject второго фактора — `user.id` (как `sub` сертификата, Story 1.8).
|
||||
*/
|
||||
@Controller('coop/2fa')
|
||||
@UseFilters(AuthV2ExceptionFilter)
|
||||
@UseGuards(HttpJwtAuthGuard)
|
||||
export class TwoFactorController {
|
||||
constructor(private readonly twoFactor: TwoFactorService) {}
|
||||
|
||||
/** Начать подключение: выпустить секрет + otpauth-URI для QR. */
|
||||
@Post('enroll')
|
||||
async enroll(@Req() req: AuthedRequest): Promise<EnrollmentChallenge> {
|
||||
const user = req.user;
|
||||
if (!user?.id) throw new UnauthorizedException();
|
||||
return this.twoFactor.beginEnrollment(user.id, user.username);
|
||||
}
|
||||
|
||||
/** Подтвердить подключение первым кодом. */
|
||||
@Post('activate')
|
||||
@HttpCode(204)
|
||||
async activate(@Body() body: CodeBody, @Req() req: AuthedRequest): Promise<void> {
|
||||
const user = req.user;
|
||||
if (!user?.id) throw new UnauthorizedException();
|
||||
if (!body?.code) throw new BadRequestException('Требуется code');
|
||||
await this.twoFactor.activate(user.id, body.code, req.ip ?? null);
|
||||
}
|
||||
|
||||
/** Отключить второй фактор (требует валидный код). */
|
||||
@Post('disable')
|
||||
@HttpCode(204)
|
||||
async disable(@Body() body: CodeBody, @Req() req: AuthedRequest): Promise<void> {
|
||||
const user = req.user;
|
||||
if (!user?.id) throw new UnauthorizedException();
|
||||
if (!body?.code) throw new BadRequestException('Требуется code');
|
||||
await this.twoFactor.disable(user.id, body.code, req.ip ?? null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
|
||||
/**
|
||||
* Транспортные детали запроса для GraphQL-резолверов auth-v2 (Фаза 2 миграции
|
||||
* REST→GraphQL/SDK). Доменные аргументы идут через `@Args`/Input-DTO; IP и
|
||||
* заголовок текущей сессии — это транспорт, не домен, поэтому достаются здесь
|
||||
* из express-`req` в GraphQL-контексте (по образцу {@link CurrentUser}), а не
|
||||
* прокидываются как GraphQL-переменные. Так IP/refresh-токен не светятся в
|
||||
* запросе/логах GraphQL, а резолвер остаётся тестируемым (значение передаётся
|
||||
* в метод напрямую — декоратор в unit-тесте не исполняется).
|
||||
*/
|
||||
|
||||
/** IP-адрес клиента (для аудита security-операций); null, если недоступен. */
|
||||
export const ClientIp = createParamDecorator((_data: unknown, context: ExecutionContext): string | null => {
|
||||
const ctx = GqlExecutionContext.create(context);
|
||||
const req = ctx.getContext().req;
|
||||
return req?.ip ?? null;
|
||||
});
|
||||
|
||||
/** Заголовок, которым SPA передаёт свой refresh-токен, чтобы пометить текущую сессию в списке. */
|
||||
export const CURRENT_SESSION_HEADER = 'x-coop-refresh-token';
|
||||
|
||||
/** Refresh-токен текущей сессии из заголовка (пометка `current` в списке сессий); null, если не передан. */
|
||||
export const RefreshTokenHeader = createParamDecorator((_data: unknown, context: ExecutionContext): string | null => {
|
||||
const ctx = GqlExecutionContext.create(context);
|
||||
const req = ctx.getContext().req;
|
||||
return req?.headers?.[CURRENT_SESSION_HEADER] ?? null;
|
||||
});
|
||||
@@ -127,6 +127,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
},
|
||||
AuthorizeDecisionInput:{
|
||||
document:"SignedDigitalDocumentInput"
|
||||
},
|
||||
AuthorizeForceRecoveryInput:{
|
||||
|
||||
},
|
||||
BankAccountDetailsInput:{
|
||||
|
||||
@@ -364,6 +367,8 @@ export const AllTypesProps: Record<string,any> = {
|
||||
CreateWithdrawInput:{
|
||||
statement:"ReturnByMoneySignedDocumentInput"
|
||||
},
|
||||
CriticalActionStatus: "enum" as const,
|
||||
CriticalActionType: "enum" as const,
|
||||
CurrentTableStatesFiltersInput:{
|
||||
|
||||
},
|
||||
@@ -444,6 +449,7 @@ export const AllTypesProps: Record<string,any> = {
|
||||
FinalizeProjectInput:{
|
||||
|
||||
},
|
||||
ForceRecoveryConsentVia: "enum" as const,
|
||||
FreeDecisionGenerateDocumentInput:{
|
||||
|
||||
},
|
||||
@@ -605,6 +611,10 @@ export const AllTypesProps: Record<string,any> = {
|
||||
Init:{
|
||||
organization_data:"CreateInitOrganizationDataInput"
|
||||
},
|
||||
InitiateCriticalActionInput:{
|
||||
action_type:"CriticalActionType",
|
||||
payload:"JSON"
|
||||
},
|
||||
Install:{
|
||||
soviet:"SovietMemberInput",
|
||||
vars:"SetVarsInput"
|
||||
@@ -647,6 +657,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
acceptChildOrder:{
|
||||
data:"AcceptChildOrderInput"
|
||||
},
|
||||
activateTwoFactor:{
|
||||
data:"TwoFactorCodeInput"
|
||||
},
|
||||
addParticipant:{
|
||||
data:"AddParticipantInput"
|
||||
},
|
||||
@@ -662,6 +675,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
authorizeDecision:{
|
||||
data:"AuthorizeDecisionInput"
|
||||
},
|
||||
authorizeForceRecovery:{
|
||||
data:"AuthorizeForceRecoveryInput"
|
||||
},
|
||||
cancelRequest:{
|
||||
data:"CancelRequestInput"
|
||||
},
|
||||
@@ -953,6 +969,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
},
|
||||
confirmAgreement:{
|
||||
data:"ConfirmAgreementInput"
|
||||
},
|
||||
confirmCriticalAction:{
|
||||
|
||||
},
|
||||
confirmReceiveOnRequest:{
|
||||
data:"ConfirmReceiveOnRequestInput"
|
||||
@@ -1017,6 +1036,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
deliverOnRequest:{
|
||||
data:"DeliverOnRequestInput"
|
||||
},
|
||||
disableTwoFactor:{
|
||||
data:"TwoFactorCodeInput"
|
||||
},
|
||||
disputeOnRequest:{
|
||||
data:"DisputeOnRequestInput"
|
||||
},
|
||||
@@ -1127,6 +1149,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
initSystem:{
|
||||
data:"Init"
|
||||
},
|
||||
initiateCriticalAction:{
|
||||
data:"InitiateCriticalActionInput"
|
||||
},
|
||||
installExtension:{
|
||||
data:"ExtensionInput"
|
||||
},
|
||||
@@ -1178,6 +1203,12 @@ export const AllTypesProps: Record<string,any> = {
|
||||
registerParticipant:{
|
||||
data:"RegisterParticipantInput"
|
||||
},
|
||||
reportNotMe:{
|
||||
data:"ReportNotMeInput"
|
||||
},
|
||||
requestForceRecoveryConsent:{
|
||||
data:"RequestForceRecoveryConsentInput"
|
||||
},
|
||||
resendNotification:{
|
||||
|
||||
},
|
||||
@@ -1190,6 +1221,12 @@ export const AllTypesProps: Record<string,any> = {
|
||||
revokeCapabilitySet:{
|
||||
data:"RevokeCapabilitySetInput"
|
||||
},
|
||||
revokeParticipantKey:{
|
||||
data:"RevokeParticipantKeyInput"
|
||||
},
|
||||
revokeSession:{
|
||||
data:"RevokeSessionInput"
|
||||
},
|
||||
saveReportDraft:{
|
||||
input:"SaveReportDraftInput"
|
||||
},
|
||||
@@ -1202,6 +1239,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
setPaymentStatus:{
|
||||
data:"SetPaymentStatusInput"
|
||||
},
|
||||
setRecoveryStrategy:{
|
||||
data:"SetRecoveryStrategyInput"
|
||||
},
|
||||
setWif:{
|
||||
data:"SetWifInput"
|
||||
},
|
||||
@@ -1554,6 +1594,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
},
|
||||
getCapitalProjectLogs:{
|
||||
data:"GetCapitalLogsInput"
|
||||
},
|
||||
getCriticalActionAuditTrail:{
|
||||
|
||||
},
|
||||
getCurrentTableStates:{
|
||||
filters:"CurrentTableStatesFiltersInput",
|
||||
@@ -1686,6 +1729,7 @@ export const AllTypesProps: Record<string,any> = {
|
||||
ReceiveOnRequestInput:{
|
||||
document:"ReturnByAssetActSignedDocumentInput"
|
||||
},
|
||||
RecoveryStrategy: "enum" as const,
|
||||
RefreshInput:{
|
||||
|
||||
},
|
||||
@@ -1719,6 +1763,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
},
|
||||
ReportHistoryFilterInput:{
|
||||
reportType:"ReportType"
|
||||
},
|
||||
ReportNotMeInput:{
|
||||
|
||||
},
|
||||
ReportPreviewInput:{
|
||||
reportType:"ReportType"
|
||||
@@ -1727,6 +1774,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
ReportType: "enum" as const,
|
||||
RepresentedByInput:{
|
||||
|
||||
},
|
||||
RequestForceRecoveryConsentInput:{
|
||||
|
||||
},
|
||||
RequisiteSource: "enum" as const,
|
||||
ResetKeyInput:{
|
||||
@@ -1788,6 +1838,12 @@ export const AllTypesProps: Record<string,any> = {
|
||||
},
|
||||
RevokeCapabilitySetInput:{
|
||||
|
||||
},
|
||||
RevokeParticipantKeyInput:{
|
||||
|
||||
},
|
||||
RevokeSessionInput:{
|
||||
|
||||
},
|
||||
RoomMessageKind: "enum" as const,
|
||||
SaveReportDraftInput:{
|
||||
@@ -1833,6 +1889,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
},
|
||||
SetPlanInput:{
|
||||
|
||||
},
|
||||
SetRecoveryStrategyInput:{
|
||||
strategy:"RecoveryStrategy"
|
||||
},
|
||||
SetVarsInput:{
|
||||
coopenomics_agreement:"AgreementVarInput",
|
||||
@@ -1897,6 +1956,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
TriggerNotificationWorkflowInput:{
|
||||
payload:"JSONObject",
|
||||
to:"NotificationWorkflowRecipientInput"
|
||||
},
|
||||
TwoFactorCodeInput:{
|
||||
|
||||
},
|
||||
UninstallExtensionInput:{
|
||||
|
||||
@@ -2019,6 +2081,14 @@ export const ReturnTypes: Record<string,any> = {
|
||||
max:"String",
|
||||
used:"String"
|
||||
},
|
||||
AccountSession:{
|
||||
created_at:"String",
|
||||
current:"Boolean",
|
||||
device:"String",
|
||||
id:"String",
|
||||
ip:"String",
|
||||
last_seen_at:"String"
|
||||
},
|
||||
AccountsPaginationResult:{
|
||||
currentPage:"Int",
|
||||
items:"Account",
|
||||
@@ -3112,6 +3182,22 @@ export const ReturnTypes: Record<string,any> = {
|
||||
question:"String",
|
||||
title:"String"
|
||||
},
|
||||
CriticalActionAuditEntry:{
|
||||
action_type:"CriticalActionType",
|
||||
confirmer_ids:"CriticalActionConfirmation",
|
||||
created_at:"String",
|
||||
finalized_at:"String",
|
||||
id:"String",
|
||||
initiated_at:"String",
|
||||
initiator_id:"String",
|
||||
payload_hash:"String",
|
||||
status:"CriticalActionStatus",
|
||||
target_id:"String"
|
||||
},
|
||||
CriticalActionConfirmation:{
|
||||
at:"String",
|
||||
by:"String"
|
||||
},
|
||||
CurrentInstanceDTO:{
|
||||
blockchain_status:"String",
|
||||
description:"String",
|
||||
@@ -3286,6 +3372,11 @@ export const ReturnTypes: Record<string,any> = {
|
||||
message:"String",
|
||||
path:"String"
|
||||
},
|
||||
ForceRecoveryAuthorization:{
|
||||
authorized:"Boolean",
|
||||
consent_via:"ForceRecoveryConsentVia",
|
||||
triggered_by:"String"
|
||||
},
|
||||
GatewayPayment:{
|
||||
blockchain_data:"JSON",
|
||||
can_change_status:"Boolean",
|
||||
@@ -3602,11 +3693,13 @@ export const ReturnTypes: Record<string,any> = {
|
||||
},
|
||||
Mutation:{
|
||||
acceptChildOrder:"Transaction",
|
||||
activateTwoFactor:"Boolean",
|
||||
addParticipant:"Account",
|
||||
addPaymentMethod:"PaymentMethod",
|
||||
addTrustedAccount:"Branch",
|
||||
assignCapabilitySet:"Boolean",
|
||||
authorizeDecision:"Transaction",
|
||||
authorizeForceRecovery:"ForceRecoveryAuthorization",
|
||||
cancelRequest:"Transaction",
|
||||
capitalAddAuthor:"CapitalProject",
|
||||
capitalApproveCommit:"CapitalCommit",
|
||||
@@ -3698,6 +3791,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
completeExtensionOnboardingStep:"ExtensionOnboardingState",
|
||||
completeRequest:"Transaction",
|
||||
confirmAgreement:"Transaction",
|
||||
confirmCriticalAction:"PendingCriticalAction",
|
||||
confirmReceiveOnRequest:"Transaction",
|
||||
confirmSupplyOnRequest:"Transaction",
|
||||
createAnnualGeneralMeet:"MeetAggregate",
|
||||
@@ -3719,8 +3813,10 @@ export const ReturnTypes: Record<string,any> = {
|
||||
deleteReportDraft:"Boolean",
|
||||
deleteTrustedAccount:"Branch",
|
||||
deliverOnRequest:"Transaction",
|
||||
disableTwoFactor:"Boolean",
|
||||
disputeOnRequest:"Transaction",
|
||||
editBranch:"Branch",
|
||||
enrollTwoFactor:"TwoFactorEnrollment",
|
||||
generateAnnualGeneralMeetAgendaDocument:"GeneratedDocument",
|
||||
generateAnnualGeneralMeetDecisionDocument:"GeneratedDocument",
|
||||
generateAnnualGeneralMeetNotificationDocument:"GeneratedDocument",
|
||||
@@ -3748,6 +3844,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
generateUserAgreement:"GeneratedDocument",
|
||||
generateWalletAgreement:"GeneratedDocument",
|
||||
initSystem:"SystemInfo",
|
||||
initiateCriticalAction:"PendingCriticalAction",
|
||||
installExtension:"Extension",
|
||||
installSystem:"SystemInfo",
|
||||
login:"RegisteredAccount",
|
||||
@@ -3765,15 +3862,21 @@ export const ReturnTypes: Record<string,any> = {
|
||||
refresh:"RegisteredAccount",
|
||||
registerAccount:"RegisteredAccount",
|
||||
registerParticipant:"Account",
|
||||
reportNotMe:"RevokedSessionsResult",
|
||||
requestForceRecoveryConsent:"Boolean",
|
||||
resendNotification:"Notification",
|
||||
resetKey:"Boolean",
|
||||
resetRegistration:"Account",
|
||||
restartAnnualGeneralMeet:"MeetAggregate",
|
||||
revokeAllSessions:"RevokedSessionsResult",
|
||||
revokeCapabilitySet:"Boolean",
|
||||
revokeParticipantKey:"RevokeKeyResult",
|
||||
revokeSession:"Boolean",
|
||||
saveReportDraft:"ReportDraft",
|
||||
selectBranch:"Boolean",
|
||||
sendAgreement:"Transaction",
|
||||
setPaymentStatus:"GatewayPayment",
|
||||
setRecoveryStrategy:"Boolean",
|
||||
setWif:"Boolean",
|
||||
signByPresiderOnAnnualGeneralMeet:"MeetAggregate",
|
||||
signBySecretaryOnAnnualGeneralMeet:"MeetAggregate",
|
||||
@@ -4085,6 +4188,18 @@ export const ReturnTypes: Record<string,any> = {
|
||||
totalCount:"Int",
|
||||
totalPages:"Int"
|
||||
},
|
||||
PendingCriticalAction:{
|
||||
action_type:"CriticalActionType",
|
||||
actor_id:"String",
|
||||
confirmations:"CriticalActionConfirmation",
|
||||
created_at:"String",
|
||||
expires_at:"String",
|
||||
finalized_at:"String",
|
||||
id:"String",
|
||||
payload:"JSON",
|
||||
status:"CriticalActionStatus",
|
||||
target_id:"String"
|
||||
},
|
||||
Permission:{
|
||||
parent:"String",
|
||||
perm_name:"String",
|
||||
@@ -4331,6 +4446,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
getCapitalOnboardingState:"CapitalOnboardingState",
|
||||
getCapitalProjectLogs:"PaginatedCapitalLogsPaginationResult",
|
||||
getChairmanOnboardingState:"ChairmanOnboardingState",
|
||||
getCriticalActionAuditTrail:"CriticalActionAuditEntry",
|
||||
getCurrentInstance:"CurrentInstanceDTO",
|
||||
getCurrentTableStates:"PaginatedCurrentTableStatesPaginationResult",
|
||||
getDeltas:"PaginatedDeltasPaginationResult",
|
||||
@@ -4360,6 +4476,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
getProgramWallets:"ProgramWalletsPaginationResult",
|
||||
getProviderSubscriptionById:"ProviderSubscription",
|
||||
getProviderSubscriptions:"ProviderSubscription",
|
||||
getRecoveryStrategy:"RecoveryStrategy",
|
||||
getRegistrationAgreements:"RegistrationAgreement",
|
||||
getRegistrationConfig:"RegistrationConfig",
|
||||
getReport:"GeneratedReport",
|
||||
@@ -4368,6 +4485,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
getReportHistory:"ReportHistoryPage",
|
||||
getReportPreview:"ReportPreview",
|
||||
getReportRequisites:"ReportRequisitesView",
|
||||
getSessions:"AccountSession",
|
||||
getSystemInfo:"SystemInfo",
|
||||
getUnreadNotificationsCount:"UnreadNotificationsCount",
|
||||
getUserWebPushSubscriptions:"WebPushSubscriptionDto",
|
||||
@@ -4539,6 +4657,15 @@ export const ReturnTypes: Record<string,any> = {
|
||||
owner:"String",
|
||||
ram_bytes:"Int"
|
||||
},
|
||||
RevokeKeyResult:{
|
||||
must_recover:"Boolean",
|
||||
sessions_revoked:"Int",
|
||||
status:"String",
|
||||
target_id:"String"
|
||||
},
|
||||
RevokedSessionsResult:{
|
||||
revoked:"Int"
|
||||
},
|
||||
SbpAccount:{
|
||||
phone:"String"
|
||||
},
|
||||
@@ -4656,6 +4783,10 @@ export const ReturnTypes: Record<string,any> = {
|
||||
startOffset:"Float",
|
||||
text:"String"
|
||||
},
|
||||
TwoFactorEnrollment:{
|
||||
otpauth_uri:"String",
|
||||
secret:"String"
|
||||
},
|
||||
UnreadNotificationsCount:{
|
||||
count:"Int"
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'activateTwoFactor'
|
||||
|
||||
/**
|
||||
* Подтвердить подключение второго фактора первым кодом.
|
||||
*/
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [{ data: $('data', 'TwoFactorCodeInput!') }, true],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
data: ModelTypes['TwoFactorCodeInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -0,0 +1,21 @@
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'disableTwoFactor'
|
||||
|
||||
/**
|
||||
* Отключить второй фактор (требует валидный код).
|
||||
*/
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [{ data: $('data', 'TwoFactorCodeInput!') }, true],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
data: ModelTypes['TwoFactorCodeInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { twoFactorEnrollmentSelector } from '../../selectors/accountSecurity/twoFactorEnrollmentSelector'
|
||||
import { type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'enrollTwoFactor'
|
||||
|
||||
/**
|
||||
* Начать подключение второго фактора: выпустить секрет и otpauth-URI для QR.
|
||||
*/
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: twoFactorEnrollmentSelector,
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -0,0 +1,20 @@
|
||||
/** Подтвердить подключение второго фактора первым кодом */
|
||||
export * as ActivateTwoFactor from './activateTwoFactor'
|
||||
|
||||
/** Отключить второй фактор */
|
||||
export * as DisableTwoFactor from './disableTwoFactor'
|
||||
|
||||
/** Начать подключение второго фактора */
|
||||
export * as EnrollTwoFactor from './enrollTwoFactor'
|
||||
|
||||
/** Сигнал «Это не я»: немедленно завершить все сессии пайщика */
|
||||
export * as ReportNotMe from './reportNotMe'
|
||||
|
||||
/** Завершить все сессии пайщика */
|
||||
export * as RevokeAllSessions from './revokeAllSessions'
|
||||
|
||||
/** Завершить конкретную сессию пайщика */
|
||||
export * as RevokeSession from './revokeSession'
|
||||
|
||||
/** Сменить стратегию восстановления */
|
||||
export * as SetRecoveryStrategy from './setRecoveryStrategy'
|
||||
@@ -0,0 +1,22 @@
|
||||
import { revokedSessionsResultSelector } from '../../selectors/accountSecurity/revokedSessionsResultSelector'
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'reportNotMe'
|
||||
|
||||
/**
|
||||
* Сигнал «Это не я»: немедленно завершить все сессии пайщика.
|
||||
*/
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [{ data: $('data', 'ReportNotMeInput!') }, revokedSessionsResultSelector],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
data: ModelTypes['ReportNotMeInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { revokedSessionsResultSelector } from '../../selectors/accountSecurity/revokedSessionsResultSelector'
|
||||
import { type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'revokeAllSessions'
|
||||
|
||||
/**
|
||||
* Завершить все сессии пайщика.
|
||||
*/
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: revokedSessionsResultSelector,
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -0,0 +1,21 @@
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'revokeSession'
|
||||
|
||||
/**
|
||||
* Завершить конкретную сессию пайщика.
|
||||
*/
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [{ data: $('data', 'RevokeSessionInput!') }, true],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
data: ModelTypes['RevokeSessionInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -0,0 +1,21 @@
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'setRecoveryStrategy'
|
||||
|
||||
/**
|
||||
* Сменить стратегию восстановления (требует step-up второго фактора).
|
||||
*/
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [{ data: $('data', 'SetRecoveryStrategyInput!') }, true],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
data: ModelTypes['SetRecoveryStrategyInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { forceRecoveryAuthorizationSelector } from '../../selectors/criticalActions/forceRecoveryAuthorizationSelector'
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'authorizeForceRecovery'
|
||||
|
||||
/**
|
||||
* Авторизовать принудительное восстановление доступа пайщика (председатель).
|
||||
*/
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [{ data: $('data', 'AuthorizeForceRecoveryInput!') }, forceRecoveryAuthorizationSelector],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
data: ModelTypes['AuthorizeForceRecoveryInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { pendingCriticalActionSelector } from '../../selectors/criticalActions/pendingCriticalActionSelector'
|
||||
import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'confirmCriticalAction'
|
||||
|
||||
/**
|
||||
* Подтвердить критическое действие совета (член совета).
|
||||
*/
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [{ id: $('id', 'String!') }, pendingCriticalActionSelector],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
id: string
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -0,0 +1,14 @@
|
||||
/** Авторизовать принудительное восстановление доступа пайщика */
|
||||
export * as AuthorizeForceRecovery from './authorizeForceRecovery'
|
||||
|
||||
/** Подтвердить критическое действие совета */
|
||||
export * as ConfirmCriticalAction from './confirmCriticalAction'
|
||||
|
||||
/** Инициировать критическое действие совета */
|
||||
export * as InitiateCriticalAction from './initiateCriticalAction'
|
||||
|
||||
/** Запросить согласие пайщика на принудительное восстановление */
|
||||
export * as RequestForceRecoveryConsent from './requestForceRecoveryConsent'
|
||||
|
||||
/** Отозвать скомпрометированный ключ пайщика */
|
||||
export * as RevokeParticipantKey from './revokeParticipantKey'
|
||||
@@ -0,0 +1,22 @@
|
||||
import { pendingCriticalActionSelector } from '../../selectors/criticalActions/pendingCriticalActionSelector'
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'initiateCriticalAction'
|
||||
|
||||
/**
|
||||
* Инициировать критическое действие совета (председатель).
|
||||
*/
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [{ data: $('data', 'InitiateCriticalActionInput!') }, pendingCriticalActionSelector],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
data: ModelTypes['InitiateCriticalActionInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -0,0 +1,21 @@
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'requestForceRecoveryConsent'
|
||||
|
||||
/**
|
||||
* Запросить согласие пайщика на принудительное восстановление (председатель).
|
||||
*/
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [{ data: $('data', 'RequestForceRecoveryConsentInput!') }, true],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
data: ModelTypes['RequestForceRecoveryConsentInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { revokeKeyResultSelector } from '../../selectors/criticalActions/revokeKeyResultSelector'
|
||||
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'revokeParticipantKey'
|
||||
|
||||
/**
|
||||
* Отозвать скомпрометированный ключ пайщика (председатель).
|
||||
*/
|
||||
export const mutation = Selector('Mutation')({
|
||||
[name]: [{ data: $('data', 'RevokeParticipantKeyInput!') }, revokeKeyResultSelector],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
data: ModelTypes['RevokeParticipantKeyInput']
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
|
||||
@@ -1,4 +1,5 @@
|
||||
export * as Accounts from './accounts'
|
||||
export * as AccountSecurity from './accountSecurity'
|
||||
export * as Agreements from './agreements'
|
||||
export * as Auth from './auth'
|
||||
export * as Authorization from './authorization'
|
||||
@@ -7,6 +8,7 @@ export * as Capital from './capital'
|
||||
export * as Chairman from './chairman'
|
||||
export * as ChatCoop from './chatcoop'
|
||||
export * as Cooplace from './cooplace'
|
||||
export * as CriticalActions from './criticalActions'
|
||||
export * as Decisions from './decisions'
|
||||
export * as Documents from './documents'
|
||||
export * as Extensions from './extensions'
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'getRecoveryStrategy'
|
||||
|
||||
/**
|
||||
* Текущая стратегия восстановления доступа пайщика (enum-значение).
|
||||
*/
|
||||
export const query = Selector('Query')({
|
||||
[name]: true,
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Query'], typeof query>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { accountSessionSelector } from '../../selectors/accountSecurity/accountSessionSelector'
|
||||
import { type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'getSessions'
|
||||
|
||||
/**
|
||||
* Активные сессии текущего пайщика (текущая помечается current).
|
||||
*/
|
||||
export const query = Selector('Query')({
|
||||
[name]: accountSessionSelector,
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Query'], typeof query>
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Текущая стратегия восстановления доступа пайщика */
|
||||
export * as GetRecoveryStrategy from './getRecoveryStrategy'
|
||||
|
||||
/** Активные сессии текущего пайщика */
|
||||
export * as GetSessions from './getSessions'
|
||||
@@ -0,0 +1,22 @@
|
||||
import { criticalActionAuditEntrySelector } from '../../selectors/criticalActions/criticalActionAuditEntrySelector'
|
||||
import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'getCriticalActionAuditTrail'
|
||||
|
||||
/**
|
||||
* Audit-trail критических действий, затрагивающих пайщика (для контролирующего органа).
|
||||
*/
|
||||
export const query = Selector('Query')({
|
||||
[name]: [{ target_id: $('target_id', 'String!') }, criticalActionAuditEntrySelector],
|
||||
})
|
||||
|
||||
export interface IInput {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
[key: string]: unknown
|
||||
|
||||
target_id: string
|
||||
}
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Query'], typeof query>
|
||||
@@ -0,0 +1,2 @@
|
||||
/** Audit-trail критических действий, затрагивающих пайщика */
|
||||
export * as GetCriticalActionAuditTrail from './getCriticalActionAuditTrail'
|
||||
@@ -1,4 +1,5 @@
|
||||
export * as Accounts from './accounts'
|
||||
export * as AccountSecurity from './accountSecurity'
|
||||
export * as Agenda from './agenda'
|
||||
export * as Agreements from './agreements'
|
||||
export * as Authorization from './authorization'
|
||||
@@ -8,6 +9,7 @@ export * as Capital from './capital'
|
||||
export * as Certificate from './certificate'
|
||||
export * as Chairman from './chairman'
|
||||
export * as ChatCoop from './chatcoop'
|
||||
export * as CriticalActions from './criticalActions'
|
||||
export * as Desktop from './desktop'
|
||||
export * as Documents from './documents'
|
||||
export * as Extensions from './extensions'
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
|
||||
import { Selector, type ValueTypes } from '../../zeus/index'
|
||||
|
||||
export const rawAccountSessionSelector = {
|
||||
id: true,
|
||||
device: true,
|
||||
ip: true,
|
||||
created_at: true,
|
||||
last_seen_at: true,
|
||||
current: true,
|
||||
}
|
||||
|
||||
// Проверка валидности
|
||||
const _validate: MakeAllFieldsRequired<ValueTypes['AccountSession']> = rawAccountSessionSelector
|
||||
|
||||
export const accountSessionSelector = Selector('AccountSession')(rawAccountSessionSelector)
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './accountSessionSelector'
|
||||
export * from './revokedSessionsResultSelector'
|
||||
export * from './twoFactorEnrollmentSelector'
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
|
||||
import { Selector, type ValueTypes } from '../../zeus/index'
|
||||
|
||||
export const rawRevokedSessionsResultSelector = {
|
||||
revoked: true,
|
||||
}
|
||||
|
||||
// Проверка валидности
|
||||
const _validate: MakeAllFieldsRequired<ValueTypes['RevokedSessionsResult']> = rawRevokedSessionsResultSelector
|
||||
|
||||
export const revokedSessionsResultSelector = Selector('RevokedSessionsResult')(rawRevokedSessionsResultSelector)
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
|
||||
import { Selector, type ValueTypes } from '../../zeus/index'
|
||||
|
||||
export const rawTwoFactorEnrollmentSelector = {
|
||||
secret: true,
|
||||
otpauth_uri: true,
|
||||
}
|
||||
|
||||
// Проверка валидности
|
||||
const _validate: MakeAllFieldsRequired<ValueTypes['TwoFactorEnrollment']> = rawTwoFactorEnrollmentSelector
|
||||
|
||||
export const twoFactorEnrollmentSelector = Selector('TwoFactorEnrollment')(rawTwoFactorEnrollmentSelector)
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
|
||||
import { Selector, type ValueTypes } from '../../zeus/index'
|
||||
import { rawCriticalActionConfirmationSelector } from './criticalActionConfirmationSelector'
|
||||
|
||||
export const rawCriticalActionAuditEntrySelector = {
|
||||
id: true,
|
||||
action_type: true,
|
||||
target_id: true,
|
||||
status: true,
|
||||
created_at: true,
|
||||
finalized_at: true,
|
||||
initiator_id: true,
|
||||
initiated_at: true,
|
||||
confirmer_ids: rawCriticalActionConfirmationSelector,
|
||||
payload_hash: true,
|
||||
}
|
||||
|
||||
// Проверка валидности
|
||||
const _validate: MakeAllFieldsRequired<ValueTypes['CriticalActionAuditEntry']> = rawCriticalActionAuditEntrySelector
|
||||
|
||||
export const criticalActionAuditEntrySelector = Selector('CriticalActionAuditEntry')(rawCriticalActionAuditEntrySelector)
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
|
||||
import { Selector, type ValueTypes } from '../../zeus/index'
|
||||
|
||||
export const rawCriticalActionConfirmationSelector = {
|
||||
by: true,
|
||||
at: true,
|
||||
}
|
||||
|
||||
// Проверка валидности
|
||||
const _validate: MakeAllFieldsRequired<ValueTypes['CriticalActionConfirmation']> = rawCriticalActionConfirmationSelector
|
||||
|
||||
export const criticalActionConfirmationSelector = Selector('CriticalActionConfirmation')(rawCriticalActionConfirmationSelector)
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
|
||||
import { Selector, type ValueTypes } from '../../zeus/index'
|
||||
|
||||
export const rawForceRecoveryAuthorizationSelector = {
|
||||
authorized: true,
|
||||
consent_via: true,
|
||||
triggered_by: true,
|
||||
}
|
||||
|
||||
// Проверка валидности
|
||||
const _validate: MakeAllFieldsRequired<ValueTypes['ForceRecoveryAuthorization']> = rawForceRecoveryAuthorizationSelector
|
||||
|
||||
export const forceRecoveryAuthorizationSelector = Selector('ForceRecoveryAuthorization')(rawForceRecoveryAuthorizationSelector)
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './criticalActionAuditEntrySelector'
|
||||
export * from './criticalActionConfirmationSelector'
|
||||
export * from './forceRecoveryAuthorizationSelector'
|
||||
export * from './pendingCriticalActionSelector'
|
||||
export * from './revokeKeyResultSelector'
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
|
||||
import { Selector, type ValueTypes } from '../../zeus/index'
|
||||
import { rawCriticalActionConfirmationSelector } from './criticalActionConfirmationSelector'
|
||||
|
||||
export const rawPendingCriticalActionSelector = {
|
||||
id: true,
|
||||
action_type: true,
|
||||
actor_id: true,
|
||||
target_id: true,
|
||||
payload: true,
|
||||
status: true,
|
||||
confirmations: rawCriticalActionConfirmationSelector,
|
||||
created_at: true,
|
||||
expires_at: true,
|
||||
finalized_at: true,
|
||||
}
|
||||
|
||||
// Проверка валидности
|
||||
const _validate: MakeAllFieldsRequired<ValueTypes['PendingCriticalAction']> = rawPendingCriticalActionSelector
|
||||
|
||||
export const pendingCriticalActionSelector = Selector('PendingCriticalAction')(rawPendingCriticalActionSelector)
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired'
|
||||
import { Selector, type ValueTypes } from '../../zeus/index'
|
||||
|
||||
export const rawRevokeKeyResultSelector = {
|
||||
status: true,
|
||||
target_id: true,
|
||||
sessions_revoked: true,
|
||||
must_recover: true,
|
||||
}
|
||||
|
||||
// Проверка валидности
|
||||
const _validate: MakeAllFieldsRequired<ValueTypes['RevokeKeyResult']> = rawRevokeKeyResultSelector
|
||||
|
||||
export const revokeKeyResultSelector = Selector('RevokeKeyResult')(rawRevokeKeyResultSelector)
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './accounts'
|
||||
export * from './accountSecurity'
|
||||
export * from './agreements'
|
||||
export * from './authorization'
|
||||
export * from './branches'
|
||||
@@ -7,6 +8,7 @@ export * from './certificate'
|
||||
export * from './chatcoop'
|
||||
export * from './common'
|
||||
export * from './cooplace'
|
||||
export * from './criticalActions'
|
||||
export * from './extensions'
|
||||
export * from './freeDecisions'
|
||||
export * from './gateway'
|
||||
|
||||
@@ -127,6 +127,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
},
|
||||
AuthorizeDecisionInput:{
|
||||
document:"SignedDigitalDocumentInput"
|
||||
},
|
||||
AuthorizeForceRecoveryInput:{
|
||||
|
||||
},
|
||||
BankAccountDetailsInput:{
|
||||
|
||||
@@ -364,6 +367,8 @@ export const AllTypesProps: Record<string,any> = {
|
||||
CreateWithdrawInput:{
|
||||
statement:"ReturnByMoneySignedDocumentInput"
|
||||
},
|
||||
CriticalActionStatus: "enum" as const,
|
||||
CriticalActionType: "enum" as const,
|
||||
CurrentTableStatesFiltersInput:{
|
||||
|
||||
},
|
||||
@@ -444,6 +449,7 @@ export const AllTypesProps: Record<string,any> = {
|
||||
FinalizeProjectInput:{
|
||||
|
||||
},
|
||||
ForceRecoveryConsentVia: "enum" as const,
|
||||
FreeDecisionGenerateDocumentInput:{
|
||||
|
||||
},
|
||||
@@ -605,6 +611,10 @@ export const AllTypesProps: Record<string,any> = {
|
||||
Init:{
|
||||
organization_data:"CreateInitOrganizationDataInput"
|
||||
},
|
||||
InitiateCriticalActionInput:{
|
||||
action_type:"CriticalActionType",
|
||||
payload:"JSON"
|
||||
},
|
||||
Install:{
|
||||
soviet:"SovietMemberInput",
|
||||
vars:"SetVarsInput"
|
||||
@@ -647,6 +657,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
acceptChildOrder:{
|
||||
data:"AcceptChildOrderInput"
|
||||
},
|
||||
activateTwoFactor:{
|
||||
data:"TwoFactorCodeInput"
|
||||
},
|
||||
addParticipant:{
|
||||
data:"AddParticipantInput"
|
||||
},
|
||||
@@ -662,6 +675,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
authorizeDecision:{
|
||||
data:"AuthorizeDecisionInput"
|
||||
},
|
||||
authorizeForceRecovery:{
|
||||
data:"AuthorizeForceRecoveryInput"
|
||||
},
|
||||
cancelRequest:{
|
||||
data:"CancelRequestInput"
|
||||
},
|
||||
@@ -953,6 +969,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
},
|
||||
confirmAgreement:{
|
||||
data:"ConfirmAgreementInput"
|
||||
},
|
||||
confirmCriticalAction:{
|
||||
|
||||
},
|
||||
confirmReceiveOnRequest:{
|
||||
data:"ConfirmReceiveOnRequestInput"
|
||||
@@ -1017,6 +1036,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
deliverOnRequest:{
|
||||
data:"DeliverOnRequestInput"
|
||||
},
|
||||
disableTwoFactor:{
|
||||
data:"TwoFactorCodeInput"
|
||||
},
|
||||
disputeOnRequest:{
|
||||
data:"DisputeOnRequestInput"
|
||||
},
|
||||
@@ -1127,6 +1149,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
initSystem:{
|
||||
data:"Init"
|
||||
},
|
||||
initiateCriticalAction:{
|
||||
data:"InitiateCriticalActionInput"
|
||||
},
|
||||
installExtension:{
|
||||
data:"ExtensionInput"
|
||||
},
|
||||
@@ -1178,6 +1203,12 @@ export const AllTypesProps: Record<string,any> = {
|
||||
registerParticipant:{
|
||||
data:"RegisterParticipantInput"
|
||||
},
|
||||
reportNotMe:{
|
||||
data:"ReportNotMeInput"
|
||||
},
|
||||
requestForceRecoveryConsent:{
|
||||
data:"RequestForceRecoveryConsentInput"
|
||||
},
|
||||
resendNotification:{
|
||||
|
||||
},
|
||||
@@ -1190,6 +1221,12 @@ export const AllTypesProps: Record<string,any> = {
|
||||
revokeCapabilitySet:{
|
||||
data:"RevokeCapabilitySetInput"
|
||||
},
|
||||
revokeParticipantKey:{
|
||||
data:"RevokeParticipantKeyInput"
|
||||
},
|
||||
revokeSession:{
|
||||
data:"RevokeSessionInput"
|
||||
},
|
||||
saveReportDraft:{
|
||||
input:"SaveReportDraftInput"
|
||||
},
|
||||
@@ -1202,6 +1239,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
setPaymentStatus:{
|
||||
data:"SetPaymentStatusInput"
|
||||
},
|
||||
setRecoveryStrategy:{
|
||||
data:"SetRecoveryStrategyInput"
|
||||
},
|
||||
setWif:{
|
||||
data:"SetWifInput"
|
||||
},
|
||||
@@ -1554,6 +1594,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
},
|
||||
getCapitalProjectLogs:{
|
||||
data:"GetCapitalLogsInput"
|
||||
},
|
||||
getCriticalActionAuditTrail:{
|
||||
|
||||
},
|
||||
getCurrentTableStates:{
|
||||
filters:"CurrentTableStatesFiltersInput",
|
||||
@@ -1686,6 +1729,7 @@ export const AllTypesProps: Record<string,any> = {
|
||||
ReceiveOnRequestInput:{
|
||||
document:"ReturnByAssetActSignedDocumentInput"
|
||||
},
|
||||
RecoveryStrategy: "enum" as const,
|
||||
RefreshInput:{
|
||||
|
||||
},
|
||||
@@ -1719,6 +1763,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
},
|
||||
ReportHistoryFilterInput:{
|
||||
reportType:"ReportType"
|
||||
},
|
||||
ReportNotMeInput:{
|
||||
|
||||
},
|
||||
ReportPreviewInput:{
|
||||
reportType:"ReportType"
|
||||
@@ -1727,6 +1774,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
ReportType: "enum" as const,
|
||||
RepresentedByInput:{
|
||||
|
||||
},
|
||||
RequestForceRecoveryConsentInput:{
|
||||
|
||||
},
|
||||
RequisiteSource: "enum" as const,
|
||||
ResetKeyInput:{
|
||||
@@ -1788,6 +1838,12 @@ export const AllTypesProps: Record<string,any> = {
|
||||
},
|
||||
RevokeCapabilitySetInput:{
|
||||
|
||||
},
|
||||
RevokeParticipantKeyInput:{
|
||||
|
||||
},
|
||||
RevokeSessionInput:{
|
||||
|
||||
},
|
||||
RoomMessageKind: "enum" as const,
|
||||
SaveReportDraftInput:{
|
||||
@@ -1833,6 +1889,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
},
|
||||
SetPlanInput:{
|
||||
|
||||
},
|
||||
SetRecoveryStrategyInput:{
|
||||
strategy:"RecoveryStrategy"
|
||||
},
|
||||
SetVarsInput:{
|
||||
coopenomics_agreement:"AgreementVarInput",
|
||||
@@ -1897,6 +1956,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
TriggerNotificationWorkflowInput:{
|
||||
payload:"JSONObject",
|
||||
to:"NotificationWorkflowRecipientInput"
|
||||
},
|
||||
TwoFactorCodeInput:{
|
||||
|
||||
},
|
||||
UninstallExtensionInput:{
|
||||
|
||||
@@ -2019,6 +2081,14 @@ export const ReturnTypes: Record<string,any> = {
|
||||
max:"String",
|
||||
used:"String"
|
||||
},
|
||||
AccountSession:{
|
||||
created_at:"String",
|
||||
current:"Boolean",
|
||||
device:"String",
|
||||
id:"String",
|
||||
ip:"String",
|
||||
last_seen_at:"String"
|
||||
},
|
||||
AccountsPaginationResult:{
|
||||
currentPage:"Int",
|
||||
items:"Account",
|
||||
@@ -3112,6 +3182,22 @@ export const ReturnTypes: Record<string,any> = {
|
||||
question:"String",
|
||||
title:"String"
|
||||
},
|
||||
CriticalActionAuditEntry:{
|
||||
action_type:"CriticalActionType",
|
||||
confirmer_ids:"CriticalActionConfirmation",
|
||||
created_at:"String",
|
||||
finalized_at:"String",
|
||||
id:"String",
|
||||
initiated_at:"String",
|
||||
initiator_id:"String",
|
||||
payload_hash:"String",
|
||||
status:"CriticalActionStatus",
|
||||
target_id:"String"
|
||||
},
|
||||
CriticalActionConfirmation:{
|
||||
at:"String",
|
||||
by:"String"
|
||||
},
|
||||
CurrentInstanceDTO:{
|
||||
blockchain_status:"String",
|
||||
description:"String",
|
||||
@@ -3286,6 +3372,11 @@ export const ReturnTypes: Record<string,any> = {
|
||||
message:"String",
|
||||
path:"String"
|
||||
},
|
||||
ForceRecoveryAuthorization:{
|
||||
authorized:"Boolean",
|
||||
consent_via:"ForceRecoveryConsentVia",
|
||||
triggered_by:"String"
|
||||
},
|
||||
GatewayPayment:{
|
||||
blockchain_data:"JSON",
|
||||
can_change_status:"Boolean",
|
||||
@@ -3602,11 +3693,13 @@ export const ReturnTypes: Record<string,any> = {
|
||||
},
|
||||
Mutation:{
|
||||
acceptChildOrder:"Transaction",
|
||||
activateTwoFactor:"Boolean",
|
||||
addParticipant:"Account",
|
||||
addPaymentMethod:"PaymentMethod",
|
||||
addTrustedAccount:"Branch",
|
||||
assignCapabilitySet:"Boolean",
|
||||
authorizeDecision:"Transaction",
|
||||
authorizeForceRecovery:"ForceRecoveryAuthorization",
|
||||
cancelRequest:"Transaction",
|
||||
capitalAddAuthor:"CapitalProject",
|
||||
capitalApproveCommit:"CapitalCommit",
|
||||
@@ -3698,6 +3791,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
completeExtensionOnboardingStep:"ExtensionOnboardingState",
|
||||
completeRequest:"Transaction",
|
||||
confirmAgreement:"Transaction",
|
||||
confirmCriticalAction:"PendingCriticalAction",
|
||||
confirmReceiveOnRequest:"Transaction",
|
||||
confirmSupplyOnRequest:"Transaction",
|
||||
createAnnualGeneralMeet:"MeetAggregate",
|
||||
@@ -3719,8 +3813,10 @@ export const ReturnTypes: Record<string,any> = {
|
||||
deleteReportDraft:"Boolean",
|
||||
deleteTrustedAccount:"Branch",
|
||||
deliverOnRequest:"Transaction",
|
||||
disableTwoFactor:"Boolean",
|
||||
disputeOnRequest:"Transaction",
|
||||
editBranch:"Branch",
|
||||
enrollTwoFactor:"TwoFactorEnrollment",
|
||||
generateAnnualGeneralMeetAgendaDocument:"GeneratedDocument",
|
||||
generateAnnualGeneralMeetDecisionDocument:"GeneratedDocument",
|
||||
generateAnnualGeneralMeetNotificationDocument:"GeneratedDocument",
|
||||
@@ -3748,6 +3844,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
generateUserAgreement:"GeneratedDocument",
|
||||
generateWalletAgreement:"GeneratedDocument",
|
||||
initSystem:"SystemInfo",
|
||||
initiateCriticalAction:"PendingCriticalAction",
|
||||
installExtension:"Extension",
|
||||
installSystem:"SystemInfo",
|
||||
login:"RegisteredAccount",
|
||||
@@ -3765,15 +3862,21 @@ export const ReturnTypes: Record<string,any> = {
|
||||
refresh:"RegisteredAccount",
|
||||
registerAccount:"RegisteredAccount",
|
||||
registerParticipant:"Account",
|
||||
reportNotMe:"RevokedSessionsResult",
|
||||
requestForceRecoveryConsent:"Boolean",
|
||||
resendNotification:"Notification",
|
||||
resetKey:"Boolean",
|
||||
resetRegistration:"Account",
|
||||
restartAnnualGeneralMeet:"MeetAggregate",
|
||||
revokeAllSessions:"RevokedSessionsResult",
|
||||
revokeCapabilitySet:"Boolean",
|
||||
revokeParticipantKey:"RevokeKeyResult",
|
||||
revokeSession:"Boolean",
|
||||
saveReportDraft:"ReportDraft",
|
||||
selectBranch:"Boolean",
|
||||
sendAgreement:"Transaction",
|
||||
setPaymentStatus:"GatewayPayment",
|
||||
setRecoveryStrategy:"Boolean",
|
||||
setWif:"Boolean",
|
||||
signByPresiderOnAnnualGeneralMeet:"MeetAggregate",
|
||||
signBySecretaryOnAnnualGeneralMeet:"MeetAggregate",
|
||||
@@ -4085,6 +4188,18 @@ export const ReturnTypes: Record<string,any> = {
|
||||
totalCount:"Int",
|
||||
totalPages:"Int"
|
||||
},
|
||||
PendingCriticalAction:{
|
||||
action_type:"CriticalActionType",
|
||||
actor_id:"String",
|
||||
confirmations:"CriticalActionConfirmation",
|
||||
created_at:"String",
|
||||
expires_at:"String",
|
||||
finalized_at:"String",
|
||||
id:"String",
|
||||
payload:"JSON",
|
||||
status:"CriticalActionStatus",
|
||||
target_id:"String"
|
||||
},
|
||||
Permission:{
|
||||
parent:"String",
|
||||
perm_name:"String",
|
||||
@@ -4331,6 +4446,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
getCapitalOnboardingState:"CapitalOnboardingState",
|
||||
getCapitalProjectLogs:"PaginatedCapitalLogsPaginationResult",
|
||||
getChairmanOnboardingState:"ChairmanOnboardingState",
|
||||
getCriticalActionAuditTrail:"CriticalActionAuditEntry",
|
||||
getCurrentInstance:"CurrentInstanceDTO",
|
||||
getCurrentTableStates:"PaginatedCurrentTableStatesPaginationResult",
|
||||
getDeltas:"PaginatedDeltasPaginationResult",
|
||||
@@ -4360,6 +4476,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
getProgramWallets:"ProgramWalletsPaginationResult",
|
||||
getProviderSubscriptionById:"ProviderSubscription",
|
||||
getProviderSubscriptions:"ProviderSubscription",
|
||||
getRecoveryStrategy:"RecoveryStrategy",
|
||||
getRegistrationAgreements:"RegistrationAgreement",
|
||||
getRegistrationConfig:"RegistrationConfig",
|
||||
getReport:"GeneratedReport",
|
||||
@@ -4368,6 +4485,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
getReportHistory:"ReportHistoryPage",
|
||||
getReportPreview:"ReportPreview",
|
||||
getReportRequisites:"ReportRequisitesView",
|
||||
getSessions:"AccountSession",
|
||||
getSystemInfo:"SystemInfo",
|
||||
getUnreadNotificationsCount:"UnreadNotificationsCount",
|
||||
getUserWebPushSubscriptions:"WebPushSubscriptionDto",
|
||||
@@ -4539,6 +4657,15 @@ export const ReturnTypes: Record<string,any> = {
|
||||
owner:"String",
|
||||
ram_bytes:"Int"
|
||||
},
|
||||
RevokeKeyResult:{
|
||||
must_recover:"Boolean",
|
||||
sessions_revoked:"Int",
|
||||
status:"String",
|
||||
target_id:"String"
|
||||
},
|
||||
RevokedSessionsResult:{
|
||||
revoked:"Int"
|
||||
},
|
||||
SbpAccount:{
|
||||
phone:"String"
|
||||
},
|
||||
@@ -4656,6 +4783,10 @@ export const ReturnTypes: Record<string,any> = {
|
||||
startOffset:"Float",
|
||||
text:"String"
|
||||
},
|
||||
TwoFactorEnrollment:{
|
||||
otpauth_uri:"String",
|
||||
secret:"String"
|
||||
},
|
||||
UnreadNotificationsCount:{
|
||||
count:"Int"
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user