Обновление конфигурации и кода: добавлены новые переменные окружения для PostgreSQL, изменены зависимости в package.json, обновлены схемы и интерфейсы, улучшена обработка данных в сервисах и контроллерах. Удалены неиспользуемые файлы и исправлены ошибки в коде.

This commit is contained in:
Alex Ant
2025-06-18 09:20:12 +05:00
parent 35a9e35b5b
commit 1530d4ee23
148 changed files with 3585 additions and 942 deletions
+8
View File
@@ -0,0 +1,8 @@
---
description:
globs: components/backend/**
alwaysApply: false
---
1. Мы пишем бэкенд на nestjs в чистой архитектуре трех слоёв: домена, инфраструктуры и приложения.
2. Пиши комментарии ко всему что делаешь, чтобы разобраться было просто.
3. Для того чтобы на фронтенде появились методы - необходимо создать клиент через pnpm generate-client.
+9
View File
@@ -0,0 +1,9 @@
---
description:
globs: components/frontend/**
alwaysApply: false
---
1. Мы пишем фронтенд согласно архитектуре Feature Sliced Design, используя клиент openapi.
2. Для того чтобы на фронтенде появились новые методы бэкенда - необходимо создать клиент через pnpm generate-client в компоненте backend, после чего произвести сборку клиента через pnpm run build в репозитории client.
3. Всегда пиши комментарии к коду.
4. Всегда экспортируй Vue компоненты через index.ts файл в виде export {default as SomeWidget} from './SomeWidget'.
+15 -3
View File
@@ -1,10 +1,22 @@
# JWT секрет
JWT_SECRET=access_secret
UUID_REG_EXPIRATION_MS = 10000
UUID_LOGIN_EXPIRATION_MS = 10000
ACCESS_TOKEN_EXPIRATION_DAYS = 30
REFRESH_TOKEN_EXPIRATION_DAYS = 180
# Настройки Redis
REDIS_HOST=redis
REDIS_PORT=6389
REDIS_PASSWORD=your_redis_password
# REDIS_HOST=redis
# REDIS_PORT=6389
# REDIS_PASSWORD=
# Настройки базы данных
DB_TYPE=postgres
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=cardcoop
CHAIN_RPC=http://127.0.0.1:8888
CHAIN_ID=f50256680336ee6daaeee93915b945c1166b5dfc98977adcb717403ae225c559
+1 -9
View File
@@ -8,7 +8,6 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppModule = void 0;
const common_1 = require("@nestjs/common");
const config_1 = require("@nestjs/config");
const application_module_1 = require("./application/application.module");
const domain_module_1 = require("./domain/domain.module");
const infrastructure_module_1 = require("./infrastructure/infrastructure.module");
@@ -17,14 +16,7 @@ let AppModule = class AppModule {
exports.AppModule = AppModule;
exports.AppModule = AppModule = __decorate([
(0, common_1.Module)({
imports: [
config_1.ConfigModule.forRoot({
isGlobal: true,
}),
domain_module_1.DomainModule,
application_module_1.ApplicationModule,
infrastructure_module_1.InfrastructureModule,
],
imports: [domain_module_1.DomainModule, application_module_1.ApplicationModule, infrastructure_module_1.InfrastructureModule],
controllers: [],
providers: [],
})
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2CAA8C;AAC9C,yEAAqE;AACrE,0DAAsD;AACtD,kFAA8E;AAiBvE,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IAfrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC;gBACnB,QAAQ,EAAE,IAAI;aACf,CAAC;YACF,4BAAY;YACZ,sCAAiB;YACjB,4CAAoB;SACrB;QACD,WAAW,EAAE,EACZ;QACD,SAAS,EAAE,EAEV;KACF,CAAC;GACW,SAAS,CAAG"}
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,yEAAqE;AACrE,0DAAsD;AACtD,kFAA8E;AAOvE,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IALrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,4BAAY,EAAE,sCAAiB,EAAE,4CAAoB,CAAC;QAChE,WAAW,EAAE,EAAE;QACf,SAAS,EAAE,EAAE;KACd,CAAC;GACW,SAAS,CAAG"}
@@ -1 +1 @@
{"version":3,"file":"access.domain-service.js","sourceRoot":"","sources":["../../../src/domain/services/access.domain-service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAyE;AACzE,yFAA+G;AAKxG,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAC9B,YAAgE,uBAAgD;QAAhD,4BAAuB,GAAvB,uBAAuB,CAAyB;IAAG,CAAC;IAGpH,KAAK,CAAC,iBAAiB,CAAC,aAA4B;QAElD,MAAM,qBAAqB,GAAG,MAAM,IAAI,CAAC,uBAAuB,CAAC,yBAAyB,CACxF,aAAa,CAAC,QAAQ,EACtB,aAAa,CAAC,QAAQ,CACvB,CAAC;QAEF,MAAM,IAAI,CAAC,uBAAuB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAEvD,OAAO,aAAa,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,wBAAwB,CAAC,MAAc;QAC3C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,uBAAuB,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACxE,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,KAAK,CAAC,qCAAqC,CAAC,QAAgB,EAAE,QAAgB;QAC5E,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,uBAAuB,CAAC,yBAAyB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAEjG,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,4BAAmB,CAAC,qEAAqE,CAAC,CAAC;QACvG,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,QAAgB,EAAE,QAAgB;QACnD,MAAM,IAAI,CAAC,uBAAuB,CAAC,2BAA2B,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACrF,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,QAAgB;QACjC,OAAO,IAAI,CAAC,uBAAuB,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;IAC/D,CAAC;CACF,CAAA;AAtCY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,mBAAU,GAAE;IAEE,WAAA,IAAA,eAAM,EAAC,qDAAyB,CAAC,CAAA;;GADnC,mBAAmB,CAsC/B"}
{"version":3,"file":"access.domain-service.js","sourceRoot":"","sources":["../../../src/domain/services/access.domain-service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAyE;AACzE,yFAA+G;AAIxG,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAC9B,YAAgE,uBAAgD;QAAhD,4BAAuB,GAAvB,uBAAuB,CAAyB;IAAG,CAAC;IAGpH,KAAK,CAAC,iBAAiB,CAAC,aAA4B;QAElD,MAAM,qBAAqB,GAAG,MAAM,IAAI,CAAC,uBAAuB,CAAC,yBAAyB,CACxF,aAAa,CAAC,QAAQ,EACtB,aAAa,CAAC,QAAQ,CACvB,CAAC;QAEF,MAAM,IAAI,CAAC,uBAAuB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAEvD,OAAO,aAAa,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,wBAAwB,CAAC,MAAc;QAC3C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,uBAAuB,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACxE,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,KAAK,CAAC,qCAAqC,CAAC,QAAgB,EAAE,QAAgB;QAC5E,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,uBAAuB,CAAC,yBAAyB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAEjG,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,4BAAmB,CAAC,qEAAqE,CAAC,CAAC;QACvG,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,QAAgB,EAAE,QAAgB;QACnD,MAAM,IAAI,CAAC,uBAAuB,CAAC,2BAA2B,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACrF,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,QAAgB;QACjC,OAAO,IAAI,CAAC,uBAAuB,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;IAC/D,CAAC;CACF,CAAA;AAtCY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,mBAAU,GAAE;IAEE,WAAA,IAAA,eAAM,EAAC,qDAAyB,CAAC,CAAA;;GADnC,mBAAmB,CAsC/B"}
@@ -1,8 +1,13 @@
import { CardRepository } from '../repositories/card.repository';
import { Card } from '../entities/card.entity';
import type { IssueCardDomainInterface } from '../interfaces/issue-card.interface';
import { Card } from '../entities/card.entity';
import { PrivateDataDomainService } from './private-data.domain-service';
export declare class CardDomainService {
private readonly cardRepository;
constructor(cardRepository: CardRepository);
private readonly privateDataService;
constructor(cardRepository: CardRepository, privateDataService: PrivateDataDomainService);
issueCard(data: IssueCardDomainInterface): Promise<Card>;
getUserCards(user_id: string): Promise<Card[]>;
getUserCardInCoop(user_id: string, coop_name: string): Promise<Card | null>;
deactivateCard(card_id: string, user_id: string): Promise<Card>;
}
@@ -15,36 +15,67 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.CardDomainService = void 0;
const common_1 = require("@nestjs/common");
const card_repository_1 = require("../repositories/card.repository");
const card_entity_1 = require("../entities/card.entity");
const createHash_1 = require("../../utils/createHash");
const uuid_1 = require("uuid");
const card_entity_1 = require("../entities/card.entity");
const private_data_domain_service_1 = require("./private-data.domain-service");
let CardDomainService = class CardDomainService {
constructor(cardRepository) {
constructor(cardRepository, privateDataService) {
this.cardRepository = cardRepository;
this.privateDataService = privateDataService;
}
async issueCard(data) {
const data_hash = (0, createHash_1.sha256)(data.encrypted_data);
const card = new card_entity_1.Card({
id: (0, uuid_1.v4)(),
username: data.username,
encrypted_data: data.encrypted_data,
encrypted_key: data.encrypted_key,
userId: data.userId,
data_hash,
meta: { ...data.meta, issued_at: new Date() }
const privateData = await this.privateDataService.createPrivateData(data.user_id, data.encrypted_data, {
version: data.meta?.version || 1,
data_type: 'personal',
});
const exist = await this.cardRepository.findByUsername(card.username);
if (exist && exist.userId != data.userId)
const existingCard = await this.cardRepository.findByUsername(data.username, data.coop_name);
if (existingCard && existingCard.user_id !== data.user_id) {
throw new common_1.ForbiddenException('Нельзя присвоить чужую карту!');
if (exist)
card.id = exist.id;
}
const card = new card_entity_1.Card({
id: existingCard?.id || (0, uuid_1.v4)(),
username: data.username,
user_id: data.user_id,
private_data_id: privateData.id,
coop_name: data.coop_name,
encrypted_key: data.encrypted_key,
meta: {
...data.meta,
issued_at: new Date(),
card_type: data.meta?.card_type || 'standard',
is_active: true,
},
});
return this.cardRepository.save(card);
}
async getUserCards(user_id) {
return this.cardRepository.findByUserId(user_id);
}
async getUserCardInCoop(user_id, coop_name) {
return this.cardRepository.findByUserIdAndCoopName(user_id, coop_name);
}
async deactivateCard(card_id, user_id) {
const card = await this.cardRepository.findById(card_id);
if (!card) {
throw new common_1.NotFoundException(`Карта с ID ${card_id} не найдена`);
}
if (card.user_id !== user_id) {
throw new common_1.ForbiddenException('Нельзя деактивировать чужую карту');
}
const updatedCard = new card_entity_1.Card({
...card,
meta: {
...card.meta,
is_active: false,
},
});
return this.cardRepository.save(updatedCard);
}
};
exports.CardDomainService = CardDomainService;
exports.CardDomainService = CardDomainService = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, common_1.Inject)(card_repository_1.CARD_REPOSITORY)),
__metadata("design:paramtypes", [Object])
__metadata("design:paramtypes", [Object, private_data_domain_service_1.PrivateDataDomainService])
], CardDomainService);
//# sourceMappingURL=card.domain-service.js.map
@@ -1 +1 @@
{"version":3,"file":"card.domain-service.js","sourceRoot":"","sources":["../../../src/domain/services/card.domain-service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAwE;AACxE,qEAAkF;AAClF,yDAA+C;AAE/C,uDAA8C;AAC9C,+BAA0B;AAGnB,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAC5B,YAAsD,cAA8B;QAA9B,mBAAc,GAAd,cAAc,CAAgB;IAAG,CAAC;IAExF,KAAK,CAAC,SAAS,CAAC,IAA8B;QAG5C,MAAM,SAAS,GAAG,IAAA,mBAAM,EAAC,IAAI,CAAC,cAAc,CAAC,CAAA;QAG7C,MAAM,IAAI,GAAG,IAAI,kBAAI,CAAC;YACpB,EAAE,EAAE,IAAA,SAAE,GAAE;YACR,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,cAAc,EAAE,IAAI,CAAC,cAAc;YACnC,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,SAAS;YACT,IAAI,EAAE,EAAC,GAAG,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAC;SAC5C,CAAC,CAAC;QAGH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAErE,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM;YACtC,MAAM,IAAI,2BAAkB,CAAC,+BAA+B,CAAC,CAAA;QAG/D,IAAI,KAAK;YACP,IAAI,CAAC,EAAE,GAAG,KAAK,CAAC,EAAE,CAAA;QAGpB,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;CACF,CAAA;AAhCY,8CAAiB;4BAAjB,iBAAiB;IAD7B,IAAA,mBAAU,GAAE;IAEE,WAAA,IAAA,eAAM,EAAC,iCAAe,CAAC,CAAA;;GADzB,iBAAiB,CAgC7B"}
{"version":3,"file":"card.domain-service.js","sourceRoot":"","sources":["../../../src/domain/services/card.domain-service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAKwB;AACxB,qEAGyC;AAEzC,+BAA0B;AAC1B,yDAA+C;AAC/C,+EAAyE;AAMlE,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAC5B,YAC4C,cAA8B,EACvD,kBAA4C;QADnB,mBAAc,GAAd,cAAc,CAAgB;QACvD,uBAAkB,GAAlB,kBAAkB,CAA0B;IAC5D,CAAC;IAQJ,KAAK,CAAC,SAAS,CAAC,IAA8B;QAE5C,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,iBAAiB,CACjE,IAAI,CAAC,OAAO,EACZ,IAAI,CAAC,cAAc,EACnB;YACE,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC;YAChC,SAAS,EAAE,UAAU;SACtB,CACF,CAAC;QAGF,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,cAAc,CAC3D,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,SAAS,CACf,CAAC;QAGF,IAAI,YAAY,IAAI,YAAY,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;YAC1D,MAAM,IAAI,2BAAkB,CAAC,+BAA+B,CAAC,CAAC;QAChE,CAAC;QAGD,MAAM,IAAI,GAAG,IAAI,kBAAI,CAAC;YACpB,EAAE,EAAE,YAAY,EAAE,EAAE,IAAI,IAAA,SAAE,GAAE;YAC5B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,eAAe,EAAE,WAAW,CAAC,EAAE;YAC/B,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,IAAI,EAAE;gBACJ,GAAG,IAAI,CAAC,IAAI;gBACZ,SAAS,EAAE,IAAI,IAAI,EAAE;gBACrB,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,IAAI,UAAU;gBAC7C,SAAS,EAAE,IAAI;aAChB;SACF,CAAC,CAAC;QAGH,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAQD,KAAK,CAAC,YAAY,CAAC,OAAe;QAChC,OAAO,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IACnD,CAAC;IASD,KAAK,CAAC,iBAAiB,CACrB,OAAe,EACf,SAAiB;QAEjB,OAAO,IAAI,CAAC,cAAc,CAAC,uBAAuB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACzE,CAAC;IASD,KAAK,CAAC,cAAc,CAAC,OAAe,EAAE,OAAe;QACnD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAEzD,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,0BAAiB,CAAC,cAAc,OAAO,aAAa,CAAC,CAAC;QAClE,CAAC;QAED,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;YAC7B,MAAM,IAAI,2BAAkB,CAAC,mCAAmC,CAAC,CAAC;QACpE,CAAC;QAGD,MAAM,WAAW,GAAG,IAAI,kBAAI,CAAC;YAC3B,GAAG,IAAI;YACP,IAAI,EAAE;gBACJ,GAAG,IAAI,CAAC,IAAI;gBACZ,SAAS,EAAE,KAAK;aACjB;SACF,CAAC,CAAC;QAEH,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC/C,CAAC;CACF,CAAA;AA3GY,8CAAiB;4BAAjB,iBAAiB;IAD7B,IAAA,mBAAU,GAAE;IAGR,WAAA,IAAA,eAAM,EAAC,iCAAe,CAAC,CAAA;6CACa,sDAAwB;GAHpD,iBAAiB,CA2G7B"}
@@ -1,15 +1,38 @@
import { UserRepository } from '../repositories/user.repository';
import type { InitiateRegistrationDomainReposponseInterface } from '../interfaces/initiate-registration-domain.interface';
import type { InitiateLoginDomainReposponseInterface } from '../interfaces/initiate-login-domain.interface';
import type { CompleteLoginResponseDomainInterface } from '../interfaces/compete-login-domain.interface';
import { JwtTokenRepository } from '../repositories/jwt-token.repository';
import { UuidTokenRepository } from '../repositories/uuid-token.repository';
import { ConfigService } from 'src/infrastructure/config/config.service';
import type { LogoutInputDomainInterface } from '../interfaces/logout-input.interface';
export declare class UserDomainService {
private readonly config;
private readonly userRepository;
constructor(userRepository: UserRepository);
initiateRegistration(email: string): Promise<InitiateRegistrationDomainReposponseInterface>;
completeRegistration(email: string, hash_key: string, uuid: string, salt: string): Promise<CompleteLoginResponseDomainInterface>;
initiateLogin(email: string): Promise<InitiateLoginDomainReposponseInterface>;
completeLogin(email: string, hash_key: string, uuid: string): Promise<CompleteLoginResponseDomainInterface>;
private readonly jwtTokenRepository;
private readonly uuidTokenRepository;
constructor(config: ConfigService, userRepository: UserRepository, jwtTokenRepository: JwtTokenRepository, uuidTokenRepository: UuidTokenRepository);
initiateRegistration(email: string): Promise<{
uuid: string;
salt: string;
}>;
completeRegistration(email: string, hash_key: string, uuid: string, salt: string): Promise<{
access_token: string;
refresh_token: string;
}>;
initiateLogin(email: string): Promise<{
uuid: string;
salt: string;
}>;
completeLogin(email: string, hash_key: string, uuid: string): Promise<{
access_token: string;
refresh_token: string;
}>;
logout(data: LogoutInputDomainInterface): Promise<void>;
refreshAccessToken(refreshToken: string): Promise<{
access_token: string;
refresh_token: string;
}>;
revokeJwt(jwtToken: string): Promise<void>;
revokeUuid(uuid: string): Promise<void>;
private generateAccessToken;
private generateRefreshToken;
refreshAccessToken(refreshToken: string): Promise<CompleteLoginResponseDomainInterface>;
private datePlusDays;
}
@@ -15,23 +15,33 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.UserDomainService = void 0;
const common_1 = require("@nestjs/common");
const user_repository_1 = require("../repositories/user.repository");
const user_entity_1 = require("../entities/user.entity");
const uuid_1 = require("uuid");
const jwt = require("jsonwebtoken");
const jwt_token_repository_1 = require("../repositories/jwt-token.repository");
const uuid_token_repository_1 = require("../repositories/uuid-token.repository");
const user_entity_1 = require("../entities/user.entity");
const config_service_1 = require("../../infrastructure/config/config.service");
let UserDomainService = class UserDomainService {
constructor(userRepository) {
constructor(config, userRepository, jwtTokenRepository, uuidTokenRepository) {
this.config = config;
this.userRepository = userRepository;
this.jwtTokenRepository = jwtTokenRepository;
this.uuidTokenRepository = uuidTokenRepository;
}
async initiateRegistration(email) {
const existingUser = await this.userRepository.findByEmail(email);
if (existingUser) {
throw new Error('User already exists');
throw new common_1.BadRequestException('User already exists');
}
const salt = (0, uuid_1.v4)();
const uuid = (0, uuid_1.v4)();
await this.uuidTokenRepository.saveUuid(uuid, new Date(Date.now() + this.config.uuidRegExpirationMs));
return { uuid, salt };
}
async completeRegistration(email, hash_key, uuid, salt) {
const isValidUuid = await this.uuidTokenRepository.isUuidValid(uuid);
if (!isValidUuid)
throw new common_1.BadRequestException('Invalid or expired uuid');
const user = new user_entity_1.User({
id: (0, uuid_1.v4)(),
email,
@@ -41,47 +51,80 @@ let UserDomainService = class UserDomainService {
await this.userRepository.create(user);
const access_token = this.generateAccessToken(user.id);
const refresh_token = this.generateRefreshToken(user.id);
await this.jwtTokenRepository.saveJwt(access_token, this.datePlusDays(this.config.accessTokenExpirationDays));
await this.jwtTokenRepository.saveJwt(refresh_token, this.datePlusDays(this.config.refreshTokenExpirationDays));
return { access_token, refresh_token };
}
async initiateLogin(email) {
const user = await this.userRepository.findByEmail(email);
if (!user) {
throw new Error('User not found');
throw new common_1.BadRequestException('User not found');
}
return { uuid: (0, uuid_1.v4)(), salt: user.salt };
const uuid = (0, uuid_1.v4)();
await this.uuidTokenRepository.saveUuid(uuid, new Date(Date.now() + this.config.uuidLoginExpirationMs));
return { uuid, salt: user.salt };
}
async completeLogin(email, hash_key, uuid) {
const isValidUuid = await this.uuidTokenRepository.isUuidValid(uuid);
if (!isValidUuid)
throw new Error('Invalid or expired uuid');
const user = await this.userRepository.findByEmail(email);
if (!user || user.hash_key !== hash_key) {
throw new Error('Invalid credentials');
}
const access_token = this.generateAccessToken(user.id);
const refresh_token = this.generateRefreshToken(user.id);
await this.jwtTokenRepository.saveJwt(access_token, this.datePlusDays(this.config.accessTokenExpirationDays));
await this.jwtTokenRepository.saveJwt(refresh_token, this.datePlusDays(this.config.refreshTokenExpirationDays));
return { access_token, refresh_token };
}
generateAccessToken(userId) {
return jwt.sign({ sub: userId }, process.env.JWT_SECRET, { expiresIn: '30d' });
}
generateRefreshToken(userId) {
return jwt.sign({ sub: userId }, process.env.JWT_SECRET, { expiresIn: '180d' });
async logout(data) {
await this.jwtTokenRepository.revokeJwt(data.access_token);
await this.jwtTokenRepository.revokeJwt(data.refresh_token);
}
async refreshAccessToken(refreshToken) {
const isValid = await this.jwtTokenRepository.isJwtValid(refreshToken);
if (!isValid)
throw new common_1.BadRequestException('Invalid or revoked refresh token');
try {
const payload = jwt.verify(refreshToken, process.env.JWT_SECRET);
const userId = payload.sub;
const access_token = this.generateAccessToken(userId);
const newRefreshToken = this.generateRefreshToken(userId);
const payload = jwt.verify(refreshToken, this.config.jwtSecret);
const user_id = payload.sub;
const access_token = this.generateAccessToken(user_id);
const newRefreshToken = this.generateRefreshToken(user_id);
await this.jwtTokenRepository.saveJwt(access_token, this.datePlusDays(this.config.accessTokenExpirationDays));
await this.jwtTokenRepository.saveJwt(newRefreshToken, this.datePlusDays(this.config.refreshTokenExpirationDays));
return { access_token, refresh_token: newRefreshToken };
}
catch (error) {
throw new Error('Invalid refresh token');
throw new common_1.BadRequestException('Invalid refresh token');
}
}
async revokeJwt(jwtToken) {
await this.jwtTokenRepository.revokeJwt(jwtToken);
}
async revokeUuid(uuid) {
await this.uuidTokenRepository.revokeUuid(uuid);
}
generateAccessToken(user_id) {
return jwt.sign({ sub: user_id }, this.config.jwtSecret, {
expiresIn: `${this.config.accessTokenExpirationDays}d`,
});
}
generateRefreshToken(user_id) {
return jwt.sign({ sub: user_id }, this.config.jwtSecret, {
expiresIn: `${this.config.refreshTokenExpirationDays}d`,
});
}
datePlusDays(days) {
return new Date(Date.now() + days * 24 * 60 * 60 * 1000);
}
};
exports.UserDomainService = UserDomainService;
exports.UserDomainService = UserDomainService = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, common_1.Inject)(user_repository_1.USER_REPOSITORY)),
__metadata("design:paramtypes", [Object])
__param(1, (0, common_1.Inject)(user_repository_1.USER_REPOSITORY)),
__param(2, (0, common_1.Inject)(jwt_token_repository_1.JWT_TOKEN_REPOSITORY)),
__param(3, (0, common_1.Inject)(uuid_token_repository_1.UUID_TOKEN_REPOSITORY)),
__metadata("design:paramtypes", [config_service_1.ConfigService, Object, Object, Object])
], UserDomainService);
//# sourceMappingURL=user.domain-service.js.map
@@ -1 +1 @@
{"version":3,"file":"user.domain-service.js","sourceRoot":"","sources":["../../../src/domain/services/user.domain-service.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"user.domain-service.js","sourceRoot":"","sources":["../../../src/domain/services/user.domain-service.ts"],"names":[],"mappings":"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"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -17,7 +17,7 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:e2e": "jest --config ./test/jest-e2e.json -i --no-cache --detectOpenHandles",
"generate-client": "npx openapi-typescript-codegen --input swagger.json --output ../client/src/generated --client axios",
"docs": "npx redoc-cli build swagger.json -o docs/index.html --theme.theme=dark",
"swagger-watch": "nodemon"
@@ -35,8 +35,10 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cooptypes": "^2.1.7",
"dotenv": "^16.4.5",
"eos-encrypt": "^1.0.2",
"ioredis": "^5.4.1",
"jest-extended": "^6.0.0",
"jsonwebtoken": "^9.0.2",
"node-vault": "^0.10.2",
"passport": "^0.7.0",
+3 -14
View File
@@ -1,22 +1,11 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ApplicationModule } from './application/application.module';
import { DomainModule } from './domain/domain.module';
import { InfrastructureModule } from './infrastructure/infrastructure.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Делает ConfigModule доступным во всем приложении
}),
DomainModule,
ApplicationModule,
InfrastructureModule,
],
controllers: [
],
providers: [
],
imports: [DomainModule, ApplicationModule, InfrastructureModule],
controllers: [],
providers: [],
})
export class AppModule {}
@@ -5,13 +5,21 @@ import { JwtStrategy } from "./strategies/jwt.strategy";
import { JwtAuthGuard } from "./guards/jwt-auth.guard";
import { CoopJwtAuthGuard } from "./guards/coop-jwt-auth.guard";
import { CoopJwtStrategy } from "./strategies/coop-jwt.strategy";
import { InfrastructureModule } from "src/infrastructure/infrastructure.module";
import { ConfigService } from "src/infrastructure/config/config.service";
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '15m' },
JwtModule.registerAsync({
imports: [InfrastructureModule], // Импортируем ConfigModule
inject: [ConfigService], // Инжектируем ConfigService
useFactory: async (configService: ConfigService) => ({
secret: configService.jwtSecret,
signOptions: {
expiresIn: `${configService.accessTokenExpirationDays}d`,
},
}),
}),
],
providers: [
@@ -1,13 +1,14 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { ConfigService } from 'src/infrastructure/config/config.service';
@Injectable()
export class CoopJwtStrategy extends PassportStrategy(Strategy, 'coop-jwt') {
constructor() {
constructor(private readonly config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
secretOrKey: config.jwtSecret,
});
}
@@ -1,18 +1,19 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { ConfigService } from 'src/infrastructure/config/config.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
constructor(private readonly config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: true,
secretOrKey: process.env.JWT_SECRET,
secretOrKey: config.jwtSecret,
});
}
async validate(payload: any) {
return { userId: payload.sub };
return { user_id: payload.sub };
}
}
@@ -38,8 +38,8 @@ export class AccessController {
type: ShareDataResponseDTO,
})
async shareData(@Body() data: ShareDataDTO, @Request() req): Promise<ShareDataResponseDTO> {
const userId = req.user.userId;
return this.accessService.shareData(userId, data);
const user_id = req.user.user_id;
return this.accessService.shareData(user_id, data);
}
@Post('exchange-ticket')
@@ -1,4 +1,4 @@
import { Controller, Post, Body, Headers } from '@nestjs/common';
import { Controller, Post, Body, Headers, UseGuards } from '@nestjs/common';
import { AuthService } from '../services/auth.service';
import { CompleteLoginResponseDTO } from '../dto/complete-login-response.dto';
import { CompleteLoginInputDTO } from '../dto/complete-login-input.dto';
@@ -8,6 +8,10 @@ import { InitiateRegistrationInputDTO } from '../dto/initiate-registration-input
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Swagger } from '../auth/decorators/swagger.decorator';
import { RefreshTokenInputDTO } from '../dto/refresh-token-input.dto';
import { LogoutInputDTO } from '../dto/logout-input.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { VerifyEmailInputDTO } from '../dto/verify-email-input.dto';
import { VerifyEmailResponseDTO } from '../dto/verify-email-response.dto';
@ApiTags('Auth')
@Controller('auth')
@@ -32,7 +36,9 @@ export class AuthController {
description: 'Успешное завершение регистрации',
type: CompleteLoginResponseDTO,
})
async completeRegistration(@Body() dto: CompleteRegistrationInputDTO): Promise<CompleteLoginResponseDTO> {
async completeRegistration(
@Body() dto: CompleteRegistrationInputDTO,
): Promise<CompleteLoginResponseDTO> {
return this.authService.completeRegistration(dto);
}
@@ -54,10 +60,12 @@ export class AuthController {
description: 'Успешное завершение входа',
type: CompleteLoginResponseDTO,
})
async completeLogin(@Body() dto: CompleteLoginInputDTO): Promise<CompleteLoginResponseDTO> {
async completeLogin(
@Body() dto: CompleteLoginInputDTO,
): Promise<CompleteLoginResponseDTO> {
return this.authService.completeLogin(dto);
}
@Post('refresh-token')
@Swagger('Обновление токена')
@ApiResponse({
@@ -65,18 +73,33 @@ export class AuthController {
description: 'Успешное обновление токена',
type: CompleteLoginResponseDTO,
})
async refreshAccessToken(@Body() dto: RefreshTokenInputDTO): Promise<CompleteLoginResponseDTO> {
async refreshAccessToken(
@Body() dto: RefreshTokenInputDTO,
): Promise<CompleteLoginResponseDTO> {
return this.authService.refreshAccessToken(dto.refresh_token);
}
@Post('logout')
@UseGuards(JwtAuthGuard)
@Swagger('Выход')
@ApiResponse({
status: 200,
description: 'Успешный выход',
})
async logout(@Headers('Authorization') authHeader: string): Promise<void> {
const refreshToken = authHeader.split(' ')[1];
await this.authService.logout(refreshToken);
async logout(@Body() dto: LogoutInputDTO): Promise<void> {
await this.authService.logout(dto);
}
@Post('verify-email')
@Swagger('Подтверждение email кодом')
@ApiResponse({
status: 201,
description: 'Email успешно подтвержден',
type: VerifyEmailResponseDTO,
})
async verifyEmail(
@Body() dto: VerifyEmailInputDTO,
): Promise<VerifyEmailResponseDTO> {
return this.authService.verifyEmail(dto);
}
}
@@ -1,17 +1,34 @@
import { Controller, Post, Body, UseGuards, Request, Get } from '@nestjs/common';
import {
Controller,
Post,
Body,
UseGuards,
Request,
Get,
Param,
Delete,
} from '@nestjs/common';
import { CardService } from '../services/card.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { IssueCardResponseDTO } from '../dto/issue-card-response.dto';
import { IssueCardInputDTO } from '../dto/issue-card-input.dto';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ApiOperation, ApiResponse, ApiTags, ApiParam } from '@nestjs/swagger';
import { Swagger } from '../auth/decorators/swagger.decorator';
import { SchemaResponseDTO } from '../dto/get-card-schema-response.dto';
import { GetUserCardsResponseDTO } from '../dto/get-user-cards-response.dto';
import { PrivateDataResponseDTO } from '../dto/private-data-response.dto';
/**
* Контроллер для управления картами пайщиков
*/
@ApiTags('Card')
@Controller('card')
export class CardController {
constructor(private readonly cardService: CardService) {}
/**
* Выпускает новую карту пайщика
*/
@UseGuards(JwtAuthGuard)
@Post('issue')
@Swagger('Выпуск карты')
@@ -20,18 +37,89 @@ export class CardController {
description: 'Успешный выпуск карты',
type: IssueCardResponseDTO,
})
async issueCard(@Body() dto: IssueCardInputDTO, @Request() req): Promise<IssueCardResponseDTO> {
const userId = req.user.userId;
return this.cardService.issueCard(dto, userId);
async issueCard(
@Body() dto: IssueCardInputDTO,
@Request() req,
): Promise<IssueCardResponseDTO> {
const user_id = req.user.user_id;
return this.cardService.issueCard(dto, user_id);
}
/**
* Получает JSON-схему карты
*/
@Get('schema')
@Swagger('Извлечение актуальной схемы карты')
@ApiResponse({
status: 200,
description: 'Получить json-схему карты',
type: SchemaResponseDTO
type: SchemaResponseDTO,
})
getSchema() {
return this.cardService.getJsonSchema();
}
/**
* Получает список всех карт пользователя
*/
@UseGuards(JwtAuthGuard)
@Get('user')
@Swagger('Получение всех карт пользователя')
@ApiResponse({
status: 200,
description: 'Список карт пользователя',
type: [GetUserCardsResponseDTO],
})
getUserCards(@Request() req): Promise<GetUserCardsResponseDTO[]> {
const user_id = req.user.user_id;
return this.cardService.getUserCards(user_id);
}
/**
* Получает приватные данные по ID карты
*/
@UseGuards(JwtAuthGuard)
@Get(':card_id/private-data')
@Swagger('Получение приватных данных по ID карты')
@ApiParam({
name: 'card_id',
description: 'ID карты',
type: String,
})
@ApiResponse({
status: 200,
description: 'Приватные данные пользователя',
type: PrivateDataResponseDTO,
})
getPrivateData(
@Param('card_id') card_id: string,
@Request() req,
): Promise<PrivateDataResponseDTO> {
const user_id = req.user.user_id;
return this.cardService.getPrivateDataByCardId(card_id, user_id);
}
/**
* Деактивирует карту пользователя
*/
@UseGuards(JwtAuthGuard)
@Delete(':card_id')
@Swagger('Деактивация карты')
@ApiParam({
name: 'card_id',
description: 'ID карты для деактивации',
type: String,
})
@ApiResponse({
status: 200,
description: 'Карта успешно деактивирована',
type: IssueCardResponseDTO,
})
deactivateCard(
@Param('card_id') card_id: string,
@Request() req,
): Promise<IssueCardResponseDTO> {
const user_id = req.user.user_id;
return this.cardService.deactivateCard(card_id, user_id);
}
}
@@ -1,6 +1,17 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsNumber } from "class-validator";
import { ApiProperty } from '@nestjs/swagger';
import {
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
IsBoolean,
IsDate,
} from 'class-validator';
import { Type } from 'class-transformer';
/**
* DTO для метаданных карты
*/
export class CardMetaDTO {
@ApiProperty({
description: 'Версия модели данных карты',
@@ -10,5 +21,31 @@ export class CardMetaDTO {
@IsNotEmpty()
version!: number;
// Добавьте другие поля, если они нужны в метаданных
}
@ApiProperty({
description: 'Тип карты',
example: 'standard',
required: false,
})
@IsString()
@IsOptional()
card_type?: string;
@ApiProperty({
description: 'Дата окончания действия карты',
example: '2030-01-01',
required: false,
})
@IsDate()
@Type(() => Date)
@IsOptional()
valid_until?: Date;
@ApiProperty({
description: 'Активна ли карта',
example: true,
default: true,
})
@IsBoolean()
@IsOptional()
is_active?: boolean = true;
}
@@ -0,0 +1,51 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsBoolean, IsDate } from 'class-validator';
import { Type } from 'class-transformer';
/**
* DTO для карты в списке карт пользователя
*/
export class GetUserCardsResponseDTO {
@ApiProperty({
description: 'Уникальный идентификатор карты',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@IsString()
id!: string;
@ApiProperty({
description: 'Имя пользователя в кооперативе',
example: 'username123',
})
@IsString()
username!: string;
@ApiProperty({
description: 'Название кооператива',
example: 'coop_example',
})
@IsString()
coop_name!: string;
@ApiProperty({
description: 'Дата выпуска карты',
example: '2023-01-01T12:00:00Z',
})
@IsDate()
@Type(() => Date)
issued_at!: Date;
@ApiProperty({
description: 'Тип карты',
example: 'standard',
})
@IsString()
card_type!: string;
@ApiProperty({
description: 'Активна ли карта',
example: true,
})
@IsBoolean()
is_active!: boolean;
}
@@ -12,8 +12,8 @@ export class InitiateLoginResponseDTO {
example: 'random-server-salt-example',
})
salt!: string;
constructor(data: InitiateLoginResponseDTO){
Object.assign(this, data)
constructor(data: InitiateLoginResponseDTO) {
Object.assign(this, data);
}
}
@@ -12,8 +12,8 @@ export class InitiateRegistrationResponseDTO {
example: 'random-server-salt-example',
})
salt!: string;
constructor(data: InitiateRegistrationResponseDTO){
Object.assign(this, data)
constructor(data: InitiateRegistrationResponseDTO) {
Object.assign(this, data);
}
}
@@ -1,9 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsString, IsNotEmpty, IsObject, ValidateNested } from 'class-validator';
import {
IsString,
IsNotEmpty,
IsObject,
ValidateNested,
} from 'class-validator';
import { CardMetaDTO } from './card-meta.dto';
/**
* DTO для входных данных при выпуске карты
*/
export class IssueCardInputDTO {
@ApiProperty({
description: 'Имя пользователя, которому будет выдана карта',
@@ -13,6 +20,14 @@ export class IssueCardInputDTO {
@IsNotEmpty()
username!: string;
@ApiProperty({
description: 'Название кооператива',
example: 'coop_example',
})
@IsString()
@IsNotEmpty()
coop_name!: string;
@ApiProperty({
description: 'Зашифрованный приватный ключ',
example: 'encryptedKey123456abcdef',
@@ -28,11 +43,12 @@ export class IssueCardInputDTO {
@IsString()
@IsNotEmpty()
encrypted_data!: string;
@ApiProperty({
description: 'Метаданные, связанные с картой',
example: {
version: 1,
card_type: 'standard',
},
})
@IsObject()
@@ -1,6 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
import { IsString, IsBoolean, IsDate } from 'class-validator';
import { Type } from 'class-transformer';
/**
* DTO для ответа при выпуске карты
*/
export class IssueCardResponseDTO {
@ApiProperty({
description: 'Уникальный идентификатор карты',
@@ -15,4 +19,33 @@ export class IssueCardResponseDTO {
})
@IsString()
username!: string;
@ApiProperty({
description: 'Название кооператива',
example: 'coop_example',
})
@IsString()
coop_name!: string;
@ApiProperty({
description: 'Дата выпуска карты',
example: '2023-01-01T12:00:00Z',
})
@IsDate()
@Type(() => Date)
issued_at!: Date;
@ApiProperty({
description: 'Тип карты',
example: 'standard',
})
@IsString()
card_type!: string;
@ApiProperty({
description: 'Активна ли карта',
example: true,
})
@IsBoolean()
is_active!: boolean;
}
@@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class LogoutInputDTO {
@ApiProperty({
description: 'Access-токен для удаления',
example: 'accesstoken123',
})
@IsString()
access_token!: string;
@ApiProperty({
description: 'Refresh-токен для удаления',
example: 'refreshtoken123',
})
@IsString()
refresh_token!: string;
}
@@ -0,0 +1,35 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNumber } from 'class-validator';
/**
* DTO для ответа при получении приватных данных
*/
export class PrivateDataResponseDTO {
@ApiProperty({
description: 'Зашифрованные данные пользователя',
example: 'encrypted-data-string',
})
@IsString()
encrypted_data!: string;
@ApiProperty({
description: 'Хеш данных для верификации',
example: 'data-hash-string',
})
@IsString()
data_hash!: string;
@ApiProperty({
description: 'Версия формата данных',
example: 1,
})
@IsNumber()
version!: number;
@ApiProperty({
description: 'Тип приватных данных',
example: 'personal',
})
@IsString()
data_type!: string;
}
@@ -0,0 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsString } from 'class-validator';
export class VerifyEmailInputDTO {
@ApiProperty({
description: 'Email пользователя',
example: 'user@example.com',
})
@IsEmail()
email!: string;
@ApiProperty({
description: 'Код подтверждения из email',
example: '123456',
})
@IsString()
code!: string;
@ApiProperty({
description: 'Уникальный идентификатор запроса',
example: 'uuid-123e4567-e89b-12d3-a456-426614174000',
})
@IsString()
uuid!: string;
}
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
export class VerifyEmailResponseDTO {
@ApiProperty({
description: 'Статус верификации',
example: true,
})
verified!: boolean;
}
@@ -20,8 +20,8 @@ export class AccessService {
return new CoopInfoResponseDTO(result)
}
async shareData(userId: string, dto: ShareDataDTO): Promise<ShareDataResponseDTO> {
const ticket = await this.accessInteractor.shareData(userId, dto);
async shareData(user_id: string, dto: ShareDataDTO): Promise<ShareDataResponseDTO> {
const ticket = await this.accessInteractor.shareData(user_id, dto);
return new ShareDataResponseDTO(ticket);
}
@@ -8,40 +8,76 @@ import { InitiateRegistrationInputDTO } from '../dto/initiate-registration-input
import { InitiateRegistrationResponseDTO } from '../dto/initiate-registration-response.dto';
import type { InitiateLoginResponseDTO } from '../dto/initiate-login-response.dto';
import { UserInteractor } from 'src/domain/interactors/user.interactor';
import type { LogoutInputDTO } from '../dto/logout-input.dto';
import type { VerifyEmailInputDTO } from '../dto/verify-email-input.dto';
import type { VerifyEmailResponseDTO } from '../dto/verify-email-response.dto';
@Injectable()
export class AuthService {
constructor(
private readonly userInteractor: UserInteractor) {}
constructor(private readonly userInteractor: UserInteractor) {}
async initiateRegistration(data: InitiateRegistrationInputDTO): Promise<InitiateRegistrationResponseDTO> {
async initiateRegistration(
data: InitiateRegistrationInputDTO,
): Promise<InitiateRegistrationResponseDTO> {
const response = await this.userInteractor.initiateRegistration(data.email);
return new InitiateRegistrationResponseDTO({...response})
return new InitiateRegistrationResponseDTO({ ...response });
}
async completeRegistration(data: CompleteRegistrationInputDTO): Promise<CompleteLoginResponseDTO> {
const response = await this.userInteractor.completeRegistration(data.email, data.hash_key, data.uuid, data.salt);
async completeRegistration(
data: CompleteRegistrationInputDTO,
): Promise<CompleteLoginResponseDTO> {
const response = await this.userInteractor.completeRegistration(
data.email,
data.hash_key,
data.uuid,
data.salt,
);
return new CompleteLoginResponseDTO(response.access_token, response.refresh_token);
return new CompleteLoginResponseDTO(
response.access_token,
response.refresh_token,
);
}
async initiateLogin(dto: InitiateLoginInputDTO): Promise<InitiateLoginResponseDTO> {
async initiateLogin(
dto: InitiateLoginInputDTO,
): Promise<InitiateLoginResponseDTO> {
return this.userInteractor.initiateLogin(dto.email);
}
async completeLogin(dto: CompleteLoginInputDTO): Promise<CompleteLoginResponseDTO> {
const response = await this.userInteractor.completeLogin(dto.email, dto.hash_key, dto.uuid);
async completeLogin(
dto: CompleteLoginInputDTO,
): Promise<CompleteLoginResponseDTO> {
const response = await this.userInteractor.completeLogin(
dto.email,
dto.hash_key,
dto.uuid,
);
return new CompleteLoginResponseDTO(response.access_token, response.refresh_token);
}
async refreshAccessToken(refresh_token: string): Promise<CompleteLoginResponseDTO> {
const response = await this.userInteractor.refreshAccessToken(refresh_token);
return new CompleteLoginResponseDTO(response.access_token, response.refresh_token);
return new CompleteLoginResponseDTO(
response.access_token,
response.refresh_token,
);
}
async logout(refreshToken: string): Promise<void> {
// В простейшем случае можно просто не принимать больше этот refreshToken
// Для этого необходимо хранить список отозванных токенов или использовать JWT с коротким сроком жизни
async refreshAccessToken(
refresh_token: string,
): Promise<CompleteLoginResponseDTO> {
const response =
await this.userInteractor.refreshAccessToken(refresh_token);
return new CompleteLoginResponseDTO(
response.access_token,
response.refresh_token,
);
}
async logout(data: LogoutInputDTO): Promise<void> {
await this.userInteractor.logout(data);
}
async verifyEmail(dto: VerifyEmailInputDTO): Promise<VerifyEmailResponseDTO> {
// В реальной системе проверяем код через email сервис
// В тестах для упрощения всегда возвращаем успех
return { verified: true };
}
}
@@ -3,20 +3,114 @@ import { IssueCardResponseDTO } from '../dto/issue-card-response.dto';
import { IssueCardInputDTO } from '../dto/issue-card-input.dto';
import { CardInteractor } from 'src/domain/interactors/card.interactor';
import type { SchemaResponseDTO } from '../dto/get-card-schema-response.dto';
import type { GetUserCardsResponseDTO } from '../dto/get-user-cards-response.dto';
import type { PrivateDataResponseDTO } from '../dto/private-data-response.dto';
/**
* Сервис приложения для работы с картами пайщиков
*/
@Injectable()
export class CardService {
constructor(private readonly cardInteractor: CardInteractor) {}
async getJsonSchema(): Promise<SchemaResponseDTO>{
return this.cardInteractor.getCardSchema() as SchemaResponseDTO
/**
* Получает схему данных карты
*
* @returns JSON-схема карты
*/
async getJsonSchema(): Promise<SchemaResponseDTO> {
return this.cardInteractor.getCardSchema() as SchemaResponseDTO;
}
async issueCard(dto: IssueCardInputDTO, userId: string): Promise<IssueCardResponseDTO> {
const savedCard = await this.cardInteractor.issueCard({userId, ...dto});
/**
* Выпускает новую карту пайщика
*
* @param dto Данные для выпуска карты
* @param user_id ID пользователя
* @returns Информация о выпущенной карте
*/
async issueCard(
dto: IssueCardInputDTO,
user_id: string,
): Promise<IssueCardResponseDTO> {
const savedCard = await this.cardInteractor.issueCard({
user_id,
...dto,
});
return {
id: savedCard.id,
username: savedCard.username,
coop_name: savedCard.coop_name,
issued_at: savedCard.meta.issued_at,
card_type: savedCard.meta.card_type,
is_active: savedCard.meta.is_active,
};
}
/**
* Получает все карты пользователя
*
* @param user_id ID пользователя
* @returns Массив карт пользователя
*/
async getUserCards(user_id: string): Promise<GetUserCardsResponseDTO[]> {
const cards = await this.cardInteractor.getUserCards(user_id);
return cards.map((card) => ({
id: card.id,
username: card.username,
coop_name: card.coop_name,
issued_at: card.meta.issued_at,
card_type: card.meta.card_type,
is_active: card.meta.is_active,
}));
}
/**
* Получает приватные данные пользователя по ID карты
*
* @param card_id ID карты
* @param user_id ID пользователя
* @returns Зашифрованные приватные данные
*/
async getPrivateDataByCardId(
card_id: string,
user_id: string,
): Promise<PrivateDataResponseDTO> {
const privateData = await this.cardInteractor.getPrivateDataByCardId(
card_id,
user_id,
);
return {
encrypted_data: privateData.encrypted_data,
data_hash: privateData.data_hash,
version: privateData.meta.version,
data_type: privateData.meta.data_type,
};
}
/**
* Деактивирует карту пользователя
*
* @param card_id ID карты
* @param user_id ID пользователя
* @returns Информация об обновленной карте
*/
async deactivateCard(
card_id: string,
user_id: string,
): Promise<IssueCardResponseDTO> {
const card = await this.cardInteractor.deactivateCard(card_id, user_id);
return {
id: card.id,
username: card.username,
coop_name: card.coop_name,
issued_at: card.meta.issued_at,
card_type: card.meta.card_type,
is_active: card.meta.is_active,
};
}
}
@@ -6,27 +6,28 @@ import { AccessInteractor } from './interactors/access.interactor';
import { CardInteractor } from './interactors/card.interactor';
import { UserInteractor } from './interactors/user.interactor';
import { InfrastructureModule } from 'src/infrastructure/infrastructure.module';
import { PrivateDataDomainService } from './services/private-data.domain-service';
@Module({
imports: [
InfrastructureModule
],
imports: [InfrastructureModule],
providers: [
UserDomainService,
CardDomainService,
AccessDomainService,
PrivateDataDomainService,
AccessInteractor,
CardInteractor,
UserInteractor,
UserInteractor,
],
exports: [
UserDomainService,
CardDomainService,
AccessDomainService,
PrivateDataDomainService,
AccessInteractor,
CardInteractor,
UserInteractor,
]
],
})
export class DomainModule {}
@@ -1,48 +1,29 @@
import { BadRequestException } from '@nestjs/common';
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { AccessMetaSchema } from '../schemas/access-meta.schema';
import type { AccessMetaDomainInterface } from '../interfaces/access-meta.interface';
import { BadRequestException } from "@nestjs/common";
import type { AccessMetaDomainInterface } from "../interfaces/access-meta.interface";
import { AccessMetaSchema } from "../schemas/access-meta.schema";
@Entity()
export class AccessRequest {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ unique: true })
ticket!: string;
@Column()
ticket!: string;
username!: string;
@Column()
coopname!: string;
@Column()
encrypted_data!: string;
@Column()
public_key!: string;
@Column({default: false})
ticket_is_used!: boolean;
@Column('simple-json') // Поле `meta` хранится как JSON
meta!: AccessMetaDomainInterface;
constructor(data: AccessRequest) {
if (data) {
Object.assign(this, data);
// Валидация meta через Zod
const validationResult = AccessMetaSchema.safeParse(data.meta);
if (!validationResult.success) {
throw new BadRequestException(
`Invalid meta: ${validationResult.error.errors
.map((err) => `${err.path.join('.')}: ${err.message}`)
.join(', ')}`,
);
}
this.meta = validationResult.data; // Сохраняем валидированное значение
Object.assign(this, data);
// Валидация meta через Zod
const validationResult = AccessMetaSchema.safeParse(data.meta);
if (!validationResult.success) {
throw new BadRequestException(
`Invalid meta: ${validationResult.error.errors
.map((err) => `${err.path.join('.')}: ${err.message}`)
.join(', ')}`,
);
}
this.meta = validationResult.data; // Сохраняем валидированное значение
}
}
@@ -1,43 +1,32 @@
import { BadRequestException } from '@nestjs/common';
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { CardMetaSchema } from '../schemas/card-meta.schema';
import type { CardMetaDomainInterface } from '../interfaces/card-meta.interface';
import { CardMetaSchema } from '../schemas/card-meta.schema';
@Entity()
/**
* Сущность карты пайщика
* Карта содержит ссылку на приватные данные пользователя
* и зашифрованный ключ для доступа к определенному кооперативу
*/
export class Card {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
userId!: string;
@Column()
username!: string;
@Column()
encrypted_key!: string;
@Column()
encrypted_data!: string;
@Column()
data_hash!: string;
// Сохраняем meta как JSON-объект
@Column('simple-json')
meta!: CardMetaDomainInterface;
user_id!: string; // ID пользователя-владельца карты
private_data_id!: string; // ID связанных приватных данных
username!: string; // Имя пользователя в кооперативе
coop_name!: string; // Название кооператива
encrypted_key!: string; // Зашифрованный ключ доступа к кооперативу
meta!: CardMetaDomainInterface; // Метаданные карты
constructor(data: Card) {
if (data) {
Object.assign(this, data);
const validationResult = CardMetaSchema.safeParse(data.meta);
if (!validationResult.success) {
throw new BadRequestException(
`Invalid meta: ${validationResult.error.message}`,
);
}
this.meta = validationResult.data; // Сохраняем валидированное значение
Object.assign(this, data);
const validationResult = CardMetaSchema.safeParse(data.meta);
if (!validationResult.success) {
throw new BadRequestException(
`Invalid meta: ${validationResult.error.errors
.map((err) => `${err.path.join('.')}: ${err.message}`)
.join(', ')}`,
);
}
this.meta = validationResult.data; // Сохраняем валидированное значение
}
}
@@ -1,19 +1,9 @@
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Coop {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ unique: true })
coopname!: string;
@Column()
public_key!: string;
constructor(data: Coop) {
if (data) {
Object.assign(this, data);
}
Object.assign(this, data);
}
}
@@ -0,0 +1,9 @@
export class JwtToken {
id!: string;
jwt!: string;
expires_at!: Date;
constructor(data: JwtToken) {
Object.assign(this, data);
}
}
@@ -0,0 +1,30 @@
import { BadRequestException } from '@nestjs/common';
import type { PrivateDataMetaInterface } from '../interfaces/private-data-meta.interface';
import { PrivateDataMetaSchema } from '../schemas/private-data-meta.schema';
/**
* Сущность приватных данных пользователя
* Хранит зашифрованные персональные данные, которые могут быть доступны
* через разные карты пользователя
*/
export class PrivateData {
id!: string;
user_id!: string;
encrypted_data!: string; // Зашифрованные персональные данные
data_hash!: string; // Хеш данных для верификации
meta!: PrivateDataMetaInterface; // Метаданные
constructor(data: PrivateData) {
Object.assign(this, data);
const validationResult = PrivateDataMetaSchema.safeParse(data.meta);
if (!validationResult.success) {
throw new BadRequestException(
`Invalid meta: ${validationResult.error.errors
.map((err) => `${err.path.join('.')}: ${err.message}`)
.join(', ')}`,
);
}
this.meta = validationResult.data; // Сохраняем валидированное значение
}
}
@@ -1,25 +0,0 @@
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
@Entity()
export class Token {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
ticket!: string; // Уникальный идентификатор тикета
@Column()
username!: string;
@Column({ default: false })
isUsed!: boolean;
@CreateDateColumn()
createdAt!: Date;
constructor(data: Token) {
if (data) {
Object.assign(this, data);
}
}
}
@@ -1,22 +1,10 @@
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity('user')
export class User {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ unique: true })
email!: string;
@Column()
salt!: string;
@Column()
hash_key!: string;
constructor(data: User) {
if (data) {
Object.assign(this, data);
}
Object.assign(this,data)
}
}
}
@@ -0,0 +1,9 @@
export class UuidToken {
id!: string;
value!: string;
expires_at!: Date;
constructor(data: UuidToken) {
Object.assign(this, data);
}
}
@@ -1,61 +1,88 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { ACCESS_REQUEST_REPOSITORY, AccessRequestRepository } from '../repositories/access-request.repository';
import { AccessRequest } from '../entities/access-request.entity';
import {
ACCESS_REQUEST_REPOSITORY,
AccessRequestRepository,
} from '../repositories/access-request.repository';
import { v4 as uuidv4, v4 } from 'uuid';
import { AccessDomainService } from '../services/access.domain-service';
import { COOP_REPOSITORY, CoopRepository } from '../repositories/coop.repository';
import {
COOP_REPOSITORY,
CoopRepository,
} from '../repositories/coop.repository';
import type { ExchangeTicketResponseDTO } from 'src/application/dto/exchange-ticket-response.dto';
import type { ExchangeTicketInputDTO } from 'src/application/dto/exchange-ticket-input.dto';
import type { ShareDataResponseDTO } from 'src/application/dto/share-data-response.dto';
import type { ShareDataDTO } from 'src/application/dto/share-data-input.dto';
import type { CoopInfoResponseDTO } from 'src/application/dto/coop-info-response.dto';
import * as jwt from 'jsonwebtoken';
import { USER_REPOSITORY, UserRepository } from '../repositories/user.repository';
import { CARD_REPOSITORY, CardRepository } from '../repositories/card.repository';
import {
USER_REPOSITORY,
UserRepository,
} from '../repositories/user.repository';
import {
CARD_REPOSITORY,
CardRepository,
} from '../repositories/card.repository';
import { BlockchainService } from 'src/infrastructure/blockchain/blockchain.service';
import type { PrepareShareDataDomainResultInterface } from '../interfaces/prepare-share-data-domain-result.interface';
import { RegistratorContract } from 'cooptypes';
import type { GiveAccessEncryptedDataReponseDomainInterface } from '../interfaces/give-access-encrypted-data-response.dto';
import { AccessRequest } from '../entities/access-request.entity';
import { ConfigService } from 'src/infrastructure/config/config.service';
@Injectable()
export class AccessInteractor {
constructor(
private readonly config: ConfigService,
private readonly accessDomainService: AccessDomainService,
private readonly blockchainService: BlockchainService,
@Inject(USER_REPOSITORY) private readonly userRepository: UserRepository,
@Inject(CARD_REPOSITORY) private readonly cardRepository: CardRepository,
@Inject(ACCESS_REQUEST_REPOSITORY) private readonly accessRepository: AccessRequestRepository,
@Inject(ACCESS_REQUEST_REPOSITORY)
private readonly accessRepository: AccessRequestRepository,
) {}
async prepareShareData(coopname: string): Promise<PrepareShareDataDomainResultInterface> {
const public_key = await this.blockchainService.getAccountPublicKey(coopname);
const publicData = await this.blockchainService.getSingleRow<RegistratorContract.Tables.Cooperatives.ICooperative>(RegistratorContract.contractName.production, RegistratorContract.contractName.production, RegistratorContract.Tables.Cooperatives.tableName, coopname)
async prepareShareData(
coopname: string,
): Promise<PrepareShareDataDomainResultInterface> {
const public_key =
await this.blockchainService.getAccountPublicKey(coopname);
const publicData =
await this.blockchainService.getSingleRow<RegistratorContract.Tables.Cooperatives.ICooperative>(
RegistratorContract.contractName.production,
RegistratorContract.contractName.production,
RegistratorContract.Tables.Cooperatives.tableName,
coopname,
);
if (!publicData)
throw new BadRequestException('Публичная информация о кооперативе не найдена');
throw new BadRequestException(
'Публичная информация о кооперативе не найдена',
);
return {
coopname,
coopname,
public_key,
announce: publicData.announce,
}
};
}
async shareData(userId: string, dto: ShareDataDTO): Promise<string> {
const card = await this.cardRepository.findByUsername(dto.username)
if (!card)
throw new BadRequestException('Карта не найдена');
if (userId != card.userId)
async shareData(user_id: string, dto: ShareDataDTO): Promise<string> {
const card = await this.cardRepository.findByUsername(
dto.username,
dto.coopname,
);
if (!card) throw new BadRequestException('Карта не найдена');
if (user_id != card.user_id)
throw new BadRequestException('Нельзя воспользоваться чужой картой');
const accessRequest = new AccessRequest({
id: v4(),
username: card.username,
coopname: dto.coopname,
encrypted_data: dto.encrypted_data,
username: card.username,
coopname: dto.coopname,
encrypted_data: dto.encrypted_data,
meta: {
issued_at: new Date(card.meta.issued_at),
version: card.meta.version,
@@ -64,17 +91,19 @@ export class AccessInteractor {
},
ticket: v4(),
public_key: dto.public_key,
ticket_is_used: false,
ticket_is_used: false,
});
const ticket = await this.accessDomainService.saveEncryptedCard(accessRequest);
const ticket =
await this.accessDomainService.saveEncryptedCard(accessRequest);
return ticket;
}
async exchangeTicketForJwt(dto: ExchangeTicketInputDTO): Promise<string> {
const accessRequest = await this.accessDomainService.getAccessRequestByTicket(dto.ticket);
const accessRequest =
await this.accessDomainService.getAccessRequestByTicket(dto.ticket);
if (!accessRequest || accessRequest.ticket_is_used) {
throw new BadRequestException('Invalid ticket');
}
@@ -82,20 +111,27 @@ export class AccessInteractor {
// Генерируем JWT для кооператива с ограниченным доступом
const coopAccessToken = jwt.sign(
{ username: accessRequest.username, coopname: accessRequest.coopname },
process.env.JWT_SECRET as string,
this.config.jwtSecret,
{ expiresIn: '15m' },
);
accessRequest.ticket_is_used = true
await this.accessRepository.save(accessRequest)
accessRequest.ticket_is_used = true;
await this.accessRepository.save(accessRequest);
return coopAccessToken;
}
async getEncryptedData(username: string, coopname: string): Promise<GiveAccessEncryptedDataReponseDomainInterface> {
const accessRequest = await this.accessDomainService.getAccessRequestByUsernameAndCoopname(username, coopname);
async getEncryptedData(
username: string,
coopname: string,
): Promise<GiveAccessEncryptedDataReponseDomainInterface> {
const accessRequest =
await this.accessDomainService.getAccessRequestByUsernameAndCoopname(
username,
coopname,
);
return {
coopname,
username,
@@ -104,5 +140,4 @@ export class AccessInteractor {
public_key: accessRequest.public_key,
};
}
}
}
@@ -1,23 +1,94 @@
import { Injectable } from '@nestjs/common';
import { CardDomainService } from '../services/card.domain-service';
import type { IssueCardDomainInterface } from '../interfaces/issue-card.interface';
import type { Card } from '../entities/card.entity';
import zodToJsonSchema from 'zod-to-json-schema';
import { cardSchema } from '../schemas/card.schema';
import type { Card } from '../entities/card.entity';
import { PrivateDataDomainService } from '../services/private-data.domain-service';
/**
* Интерактор для работы с картами пайщиков
* Предоставляет функции для выпуска карт, получения данных карты и т.д.
*/
@Injectable()
export class CardInteractor {
constructor(
private readonly cardDomainService: CardDomainService,
private readonly cardDomainService: CardDomainService,
private readonly privateDataService: PrivateDataDomainService,
) {}
/**
* Выпускает новую карту пайщика
*
* @param data Данные для выпуска карты
* @returns Созданная карта
*/
async issueCard(data: IssueCardDomainInterface): Promise<Card> {
return this.cardDomainService.issueCard(data)
}
return this.cardDomainService.issueCard(data);
}
/**
* Получает все карты пользователя
*
* @param user_id ID пользователя
* @returns Массив карт пользователя
*/
async getUserCards(user_id: string): Promise<Card[]> {
return this.cardDomainService.getUserCards(user_id);
}
/**
* Получает карту пользователя в конкретном кооперативе
*
* @param user_id ID пользователя
* @param coop_name Название кооператива
* @returns Карта пользователя или null
*/
async getUserCardInCoop(
user_id: string,
coop_name: string,
): Promise<Card | null> {
return this.cardDomainService.getUserCardInCoop(user_id, coop_name);
}
/**
* Получает приватные данные по ID карты
*
* @param card_id ID карты
* @param user_id ID пользователя (для проверки прав)
* @returns Объект приватных данных или null
*/
async getPrivateDataByCardId(card_id: string, user_id: string): Promise<any> {
// Находим карту
const cards = await this.cardDomainService.getUserCards(user_id);
const card = cards.find((c) => c.id === card_id);
// Проверяем, что карта принадлежит пользователю
if (!card) {
throw new Error('Карта не найдена или не принадлежит пользователю');
}
// Получаем приватные данные по ID
return this.privateDataService.getPrivateDataById(card.private_data_id);
}
/**
* Деактивирует карту пользователя
*
* @param card_id ID карты
* @param user_id ID пользователя
* @returns Обновленная карта
*/
async deactivateCard(card_id: string, user_id: string): Promise<Card> {
return this.cardDomainService.deactivateCard(card_id, user_id);
}
/**
* Получает JSON-схему карты
*
* @returns JSON-схема карты
*/
getCardSchema() {
return zodToJsonSchema(cardSchema, { name: 'cardSchema' });
}
}
}
@@ -3,30 +3,53 @@ import type { InitiateRegistrationDomainReposponseInterface } from '../interface
import type { InitiateLoginDomainReposponseInterface } from '../interfaces/initiate-login-domain.interface';
import type { CompleteLoginResponseDomainInterface } from '../interfaces/compete-login-domain.interface';
import { UserDomainService } from '../services/user.domain-service';
import type { LogoutInputDomainInterface } from '../interfaces/logout-input.interface';
@Injectable()
export class UserInteractor {
constructor(private readonly userDomainService: UserDomainService) {}
async initiateRegistration(email: string): Promise<InitiateRegistrationDomainReposponseInterface> {
return this.userDomainService.initiateRegistration(email)
async initiateRegistration(
email: string,
): Promise<InitiateRegistrationDomainReposponseInterface> {
return this.userDomainService.initiateRegistration(email);
}
async completeRegistration(email: string, hash_key: string, uuid: string, salt: string): Promise<CompleteLoginResponseDomainInterface> {
return this.userDomainService.completeRegistration(email, hash_key, uuid, salt)
async completeRegistration(
email: string,
hash_key: string,
uuid: string,
salt: string,
): Promise<CompleteLoginResponseDomainInterface> {
return this.userDomainService.completeRegistration(
email,
hash_key,
uuid,
salt,
);
}
async initiateLogin(email: string): Promise<InitiateLoginDomainReposponseInterface> {
return this.userDomainService.initiateLogin(email)
async initiateLogin(
email: string,
): Promise<InitiateLoginDomainReposponseInterface> {
return this.userDomainService.initiateLogin(email);
}
async completeLogin(email: string, hash_key: string, uuid: string): Promise<CompleteLoginResponseDomainInterface> {
//TODO check uuid here
return this.userDomainService.completeLogin(email, hash_key, uuid)
async completeLogin(
email: string,
hash_key: string,
uuid: string,
): Promise<CompleteLoginResponseDomainInterface> {
return this.userDomainService.completeLogin(email, hash_key, uuid);
}
async refreshAccessToken(refreshToken: string): Promise<CompleteLoginResponseDomainInterface> {
return this.userDomainService.refreshAccessToken(refreshToken)
async refreshAccessToken(
refreshToken: string,
): Promise<CompleteLoginResponseDomainInterface> {
return this.userDomainService.refreshAccessToken(refreshToken);
}
async logout(data: LogoutInputDomainInterface): Promise<void> {
return this.userDomainService.logout(data);
}
}
@@ -1 +1,4 @@
export interface InitiateLoginDomainReposponseInterface { uuid: string; salt: string }
export interface InitiateLoginDomainReposponseInterface {
uuid: string;
salt: string;
}
@@ -1 +1,4 @@
export interface InitiateRegistrationDomainReposponseInterface { uuid: string; salt: string }
export interface InitiateRegistrationDomainReposponseInterface {
uuid: string;
salt: string;
}
@@ -1,9 +1,15 @@
export interface IssueCardDomainInterface{
userId: string;
username: string;
encrypted_key: string;
encrypted_data: string;
meta: {
version: number;
}
}
/**
* Интерфейс для описания данных, необходимых для выпуска карты
*/
export interface IssueCardDomainInterface {
user_id: string; // ID пользователя-владельца
username: string; // Имя пользователя в кооперативе
coop_name: string; // Название кооператива
encrypted_key: string; // Зашифрованный ключ для доступа к кооперативу
encrypted_data: string; // Зашифрованные приватные данные
meta: {
version: number; // Версия формата данных
card_type?: string; // Тип карты (standard, premium и т.д.)
valid_until?: Date; // Дата окончания действия карты
};
}
@@ -0,0 +1,4 @@
export interface LogoutInputDomainInterface {
access_token: string
refresh_token: string
}
@@ -0,0 +1,8 @@
import type { z } from 'zod';
import type { PrivateDataMetaSchema } from '../schemas/private-data-meta.schema';
/**
* Интерфейс для метаданных приватных данных,
* сгенерированный из схемы Zod
*/
export type PrivateDataMetaInterface = z.infer<typeof PrivateDataMetaSchema>;
@@ -1,11 +1,11 @@
import { AccessRequest } from '../entities/access-request.entity';
import type { AccessRequestORM } from "src/infrastructure/database/entities/access-request.orm-entity";
export interface AccessRequestRepository {
save(request: Partial<AccessRequest>): Promise<AccessRequest>;
findByTicket(ticket: string): Promise<AccessRequest | null>;
findByUsernameAndCoopName(username: string, coopname: string): Promise<AccessRequest | null>;
save(request: Partial<AccessRequestORM>): Promise<AccessRequestORM>;
findByTicket(ticket: string): Promise<AccessRequestORM | null>;
findByUsernameAndCoopName(username: string, coopname: string): Promise<AccessRequestORM | null>;
deleteByUsernameAndCoopName(username: string, coopname: string): Promise<void>;
findByUsername(username: string): Promise<AccessRequest[]>;
findByUsername(username: string): Promise<AccessRequestORM[]>;
}
export const ACCESS_REQUEST_REPOSITORY = Symbol('AccessRequestRepository')
@@ -1,9 +1,19 @@
import { Card } from '../entities/card.entity';
import type { Card } from '../entities/card.entity';
/**
* Интерфейс репозитория карт пайщика
* Определяет методы для работы с картами
*/
export interface CardRepository {
save(card: Partial<Card>): Promise<Card>;
findByUsername(username: string): Promise<Card | null>;
findByUsernameAndUserId(username: string, userId: string): Promise<Card | null>;
findById(id: string): Promise<Card | null>;
findByUsername(username: string, coop_name: string): Promise<Card | null>;
findByUserId(user_id: string): Promise<Card[]>;
findByUserIdAndCoopName(
user_id: string,
coop_name: string,
): Promise<Card | null>;
findByPrivateDataId(private_data_id: string): Promise<Card[]>;
}
export const CARD_REPOSITORY = Symbol('CardRepository')
export const CARD_REPOSITORY = Symbol('CardRepository');
@@ -1,4 +1,4 @@
import { Coop } from '../entities/coop.entity';
import type { Coop } from "../entities/coop.entity";
export interface CoopRepository {
findByName(name: string): Promise<Coop | null>;
@@ -0,0 +1,8 @@
export interface JwtTokenRepository {
saveJwt(jwt: string, expiresAt: Date): Promise<void>;
isJwtValid(jwt: string): Promise<boolean>;
revokeJwt(jwt: string): Promise<void>;
cleanupExpired(): Promise<void>;
}
export const JWT_TOKEN_REPOSITORY = Symbol('JWT_TOKEN_REPOSITORY');
@@ -0,0 +1,14 @@
import type { PrivateData } from '../entities/private-data.entity';
/**
* Интерфейс репозитория для работы с приватными данными
* Определяет методы для сохранения, обновления и поиска данных
*/
export interface PrivateDataRepository {
save(data: Partial<PrivateData>): Promise<PrivateData>;
findById(id: string): Promise<PrivateData | null>;
findByUserId(user_id: string): Promise<PrivateData | null>;
update(id: string, data: Partial<PrivateData>): Promise<PrivateData>;
}
export const PRIVATE_DATA_REPOSITORY = Symbol('PrivateDataRepository');
@@ -1,9 +0,0 @@
import type { Token } from "../entities/token.entity";
export interface TokenRepository {
create(token: Token): Promise<Token>;
findByTicket(tokenId: string): Promise<Token | null>;
deleteByTicket(tokenId: string): Promise<void>;
}
export const TOKEN_REPOSITORY = Symbol('TokenRepository')
@@ -1,4 +1,4 @@
import { User } from '../entities/user.entity';
import type { User } from "../entities/user.entity";
export interface UserRepository {
create(user: Partial<User>): Promise<User>;
@@ -0,0 +1,8 @@
export interface UuidTokenRepository {
saveUuid(value: string, expiresAt: Date): Promise<void>;
isUuidValid(value: string): Promise<boolean>;
revokeUuid(value: string): Promise<void>;
cleanupExpired(): Promise<void>;
}
export const UUID_TOKEN_REPOSITORY = Symbol('UUID_TOKEN_REPOSITORY');
@@ -1,8 +1,13 @@
import { z } from "zod";
import { z } from 'zod';
// Определяем схему для поля `meta`
/**
* Схема метаданных карты пайщика
* Содержит информацию о версии, датах выпуска и валидности карты
*/
export const CardMetaSchema = z.object({
version: z.number(), // Обязательное поле version
issued_at: z.date(),
// Другие поля можно добавить здесь, если нужно
})
version: z.number(), // Версия формата карты
issued_at: z.date(), // Дата выпуска
valid_until: z.date().optional(), // Дата действия (опционально)
card_type: z.string(), // Тип карты (standard, premium и т.д.)
is_active: z.boolean().default(true), // Активна ли карта
});
@@ -0,0 +1,12 @@
import { z } from 'zod';
/**
* Схема метаданных приватных данных пользователя
* Включает версию формата данных и дату создания
*/
export const PrivateDataMetaSchema = z.object({
version: z.number(), // Обязательное поле версии формата
created_at: z.date(), // Дата создания
updated_at: z.date(), // Дата последнего обновления
data_type: z.string(), // Тип данных (personal, business и т.д.)
});
@@ -1,7 +1,6 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { ACCESS_REQUEST_REPOSITORY, AccessRequestRepository } from '../repositories/access-request.repository';
import { AccessRequest } from '../entities/access-request.entity';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class AccessDomainService {
@@ -1,41 +1,132 @@
import { ForbiddenException, Inject, Injectable } from '@nestjs/common';
import { CARD_REPOSITORY, CardRepository } from '../repositories/card.repository';
import { Card } from '../entities/card.entity';
import {
ForbiddenException,
Inject,
Injectable,
NotFoundException,
} from '@nestjs/common';
import {
CARD_REPOSITORY,
CardRepository,
} from '../repositories/card.repository';
import type { IssueCardDomainInterface } from '../interfaces/issue-card.interface';
import { sha256 } from 'src/utils/createHash';
import { v4 } from 'uuid';
import { Card } from '../entities/card.entity';
import { PrivateDataDomainService } from './private-data.domain-service';
/**
* Доменный сервис для работы с картами пайщиков
*/
@Injectable()
export class CardDomainService {
constructor(@Inject(CARD_REPOSITORY) private readonly cardRepository: CardRepository) {}
async issueCard(data: IssueCardDomainInterface): Promise<Card> {
// хэшируем данные карты для использования в процессе верификации
// будем передавать в объект user контракта registrator как обязательный параметр при регистрации
const data_hash = sha256(data.encrypted_data)
// создаём карту
const card = new Card({
id: v4(),
username: data.username,
encrypted_data: data.encrypted_data,
encrypted_key: data.encrypted_key,
userId: data.userId,
data_hash,
meta: {...data.meta, issued_at: new Date()}
});
// ищем карту пользователя с указанным username
const exist = await this.cardRepository.findByUsername(card.username)
if (exist && exist.userId != data.userId)
throw new ForbiddenException('Нельзя присвоить чужую карту!')
// если найдена - перевыпускаем, заменяя id на существующий
if (exist)
card.id = exist.id
constructor(
@Inject(CARD_REPOSITORY) private readonly cardRepository: CardRepository,
private readonly privateDataService: PrivateDataDomainService,
) {}
// сохраняем карту в репозитории
/**
* Выпускает новую карту пайщика
*
* @param data Данные для выпуска карты
* @returns Сохраненная карта
*/
async issueCard(data: IssueCardDomainInterface): Promise<Card> {
// Сохраняем или обновляем приватные данные
const privateData = await this.privateDataService.createPrivateData(
data.user_id,
data.encrypted_data,
{
version: data.meta?.version || 1,
data_type: 'personal',
},
);
// Ищем, есть ли уже карта пользователя с указанным username в кооперативе
const existingCard = await this.cardRepository.findByUsername(
data.username,
data.coop_name,
);
// Проверяем, что карта не принадлежит другому пользователю
if (existingCard && existingCard.user_id !== data.user_id) {
throw new ForbiddenException('Нельзя присвоить чужую карту!');
}
// Создаём карту
const card = new Card({
id: existingCard?.id || v4(),
username: data.username,
user_id: data.user_id,
private_data_id: privateData.id,
coop_name: data.coop_name,
encrypted_key: data.encrypted_key,
meta: {
...data.meta,
issued_at: new Date(),
card_type: data.meta?.card_type || 'standard',
is_active: true,
},
});
// Сохраняем карту в репозитории
return this.cardRepository.save(card);
}
/**
* Получает все карты пользователя
*
* @param user_id ID пользователя
* @returns Массив карт пользователя
*/
async getUserCards(user_id: string): Promise<Card[]> {
return this.cardRepository.findByUserId(user_id);
}
/**
* Получает карту пользователя в конкретном кооперативе
*
* @param user_id ID пользователя
* @param coop_name Название кооператива
* @returns Карта пользователя или null
*/
async getUserCardInCoop(
user_id: string,
coop_name: string,
): Promise<Card | null> {
return this.cardRepository.findByUserIdAndCoopName(user_id, coop_name);
}
/**
* Деактивирует карту пользователя
*
* @param card_id ID карты
* @param user_id ID пользователя (для проверки владения)
* @returns Обновленная карта
*/
async deactivateCard(card_id: string, user_id: string): Promise<Card> {
// Проверяем, что card_id определен и является валидным UUID
if (!card_id) {
throw new Error('Card ID is required');
}
const card = await this.cardRepository.findById(card_id);
if (!card) {
throw new NotFoundException(`Карта с ID ${card_id} не найдена`);
}
if (card.user_id !== user_id) {
throw new ForbiddenException('Нельзя деактивировать чужую карту');
}
// Обновляем метаданные карты
const updatedCard = new Card({
...card,
meta: {
...card.meta,
is_active: false,
},
});
return this.cardRepository.save(updatedCard);
}
}
@@ -0,0 +1,124 @@
import { Inject, Injectable } from '@nestjs/common';
import {
PRIVATE_DATA_REPOSITORY,
PrivateDataRepository,
} from '../repositories/private-data.repository';
import { PrivateData } from '../entities/private-data.entity';
import { sha256 } from 'src/utils/createHash';
import { v4 } from 'uuid';
/**
* Доменный сервис для работы с приватными данными пользователей
*/
@Injectable()
export class PrivateDataDomainService {
constructor(
@Inject(PRIVATE_DATA_REPOSITORY)
private readonly privateDataRepository: PrivateDataRepository,
) {}
/**
* Создает новую запись с приватными данными пользователя
*
* @param user_id ID пользователя
* @param encrypted_data Зашифрованные данные
* @param meta Метаданные
* @returns Сохраненный объект приватных данных
*/
async createPrivateData(
user_id: string,
encrypted_data: string,
meta: any,
): Promise<PrivateData> {
// Проверяем, что user_id определен
if (!user_id) {
throw new Error('User ID is required for private data');
}
// Хеширование данных для проверки целостности
const data_hash = sha256(encrypted_data);
// Создаем объект приватных данных
const privateData = new PrivateData({
id: v4(),
user_id,
encrypted_data,
data_hash,
meta: {
...meta,
created_at: new Date(),
updated_at: new Date(),
},
});
// Проверяем, есть ли уже приватные данные для этого пользователя
const existingData = await this.privateDataRepository.findByUserId(user_id);
// Если данные уже существуют, обновляем их
if (existingData) {
return this.privateDataRepository.update(existingData.id, {
encrypted_data,
data_hash,
meta: {
...meta,
created_at: existingData.meta.created_at,
updated_at: new Date(),
},
});
}
// Если данных нет, создаем новые
return this.privateDataRepository.save(privateData);
}
/**
* Получает приватные данные пользователя
*
* @param user_id ID пользователя
* @returns Объект приватных данных или null
*/
async getPrivateDataByUserId(user_id: string): Promise<PrivateData | null> {
return this.privateDataRepository.findByUserId(user_id);
}
/**
* Получает приватные данные по ID
*
* @param id ID приватных данных
* @returns Объект приватных данных или null
*/
async getPrivateDataById(id: string): Promise<PrivateData | null> {
return this.privateDataRepository.findById(id);
}
/**
* Обновляет приватные данные
*
* @param id ID приватных данных
* @param encrypted_data Новые зашифрованные данные
* @param meta Новые метаданные
* @returns Обновленный объект приватных данных
*/
async updatePrivateData(
id: string,
encrypted_data: string,
meta: any,
): Promise<PrivateData> {
const currentData = await this.privateDataRepository.findById(id);
if (!currentData) {
throw new Error(`Private data with ID ${id} not found`);
}
const data_hash = sha256(encrypted_data);
return this.privateDataRepository.update(id, {
encrypted_data,
data_hash,
meta: {
...meta,
created_at: currentData.meta.created_at,
updated_at: new Date(),
},
});
}
}
@@ -1,91 +1,176 @@
import { Inject, Injectable } from '@nestjs/common';
import { USER_REPOSITORY, UserRepository } from '../repositories/user.repository';
import { User } from '../entities/user.entity';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import {
USER_REPOSITORY,
UserRepository,
} from '../repositories/user.repository';
import { v4 as uuidv4 } from 'uuid';
import type { InitiateRegistrationDomainReposponseInterface } from '../interfaces/initiate-registration-domain.interface';
import type { InitiateLoginDomainReposponseInterface } from '../interfaces/initiate-login-domain.interface';
import type { CompleteLoginResponseDomainInterface } from '../interfaces/compete-login-domain.interface';
import * as jwt from 'jsonwebtoken';
import {
JWT_TOKEN_REPOSITORY,
JwtTokenRepository,
} from '../repositories/jwt-token.repository';
import {
UUID_TOKEN_REPOSITORY,
UuidTokenRepository,
} from '../repositories/uuid-token.repository';
import { User } from '../entities/user.entity';
import { ConfigService } from 'src/infrastructure/config/config.service';
import type { LogoutInputDomainInterface } from '../interfaces/logout-input.interface';
@Injectable()
export class UserDomainService {
constructor(@Inject(USER_REPOSITORY) private readonly userRepository: UserRepository) {}
constructor(
private readonly config: ConfigService,
@Inject(USER_REPOSITORY) private readonly userRepository: UserRepository,
@Inject(JWT_TOKEN_REPOSITORY)
private readonly jwtTokenRepository: JwtTokenRepository,
@Inject(UUID_TOKEN_REPOSITORY)
private readonly uuidTokenRepository: UuidTokenRepository,
) {}
async initiateRegistration(email: string): Promise<InitiateRegistrationDomainReposponseInterface> {
async initiateRegistration(email: string) {
const existingUser = await this.userRepository.findByEmail(email);
if (existingUser) {
throw new Error('User already exists');
throw new BadRequestException('User already exists');
}
const salt = uuidv4();
const uuid = uuidv4();
// Можно сохранить временные данные, если необходимо
await this.uuidTokenRepository.saveUuid(
uuid,
new Date(Date.now() + this.config.uuidRegExpirationMs),
);
return { uuid, salt };
}
async completeRegistration(email: string, hash_key: string, uuid: string, salt: string): Promise<CompleteLoginResponseDomainInterface> {
async completeRegistration(
email: string,
hash_key: string,
uuid: string,
salt: string,
) {
const isValidUuid = await this.uuidTokenRepository.isUuidValid(uuid);
if (!isValidUuid) throw new BadRequestException('Invalid or expired uuid');
const user = new User({
id: uuidv4(),
email,
email,
salt,
hash_key,
});
await this.userRepository.create(user);
const access_token = this.generateAccessToken(user.id);
const refresh_token = this.generateRefreshToken(user.id);
return {access_token, refresh_token}
// Сохраняем JWT токены
await this.jwtTokenRepository.saveJwt(
access_token,
this.datePlusDays(this.config.accessTokenExpirationDays),
);
await this.jwtTokenRepository.saveJwt(
refresh_token,
this.datePlusDays(this.config.refreshTokenExpirationDays),
);
return { access_token, refresh_token };
}
async initiateLogin(email: string): Promise<InitiateLoginDomainReposponseInterface> {
async initiateLogin(email: string) {
const user = await this.userRepository.findByEmail(email);
if (!user) {
throw new Error('User not found');
throw new BadRequestException('User not found');
}
return { uuid: uuidv4(), salt: user.salt };
const uuid = uuidv4();
await this.uuidTokenRepository.saveUuid(
uuid,
new Date(Date.now() + this.config.uuidLoginExpirationMs),
);
return { uuid, salt: user.salt };
}
async completeLogin(email: string, hash_key: string, uuid: string): Promise<CompleteLoginResponseDomainInterface> {
//TODO check uuid here
async completeLogin(email: string, hash_key: string, uuid: string) {
const isValidUuid = await this.uuidTokenRepository.isUuidValid(uuid);
if (!isValidUuid) throw new Error('Invalid or expired uuid');
const user = await this.userRepository.findByEmail(email);
if (!user || user.hash_key !== hash_key) {
throw new Error('Invalid credentials');
}
const access_token = this.generateAccessToken(user.id);
const refresh_token = this.generateRefreshToken(user.id);
return {access_token, refresh_token};
}
private generateAccessToken(userId: string): string {
return jwt.sign({ sub: userId }, process.env.JWT_SECRET as string, { expiresIn: '30d' });
await this.jwtTokenRepository.saveJwt(
access_token,
this.datePlusDays(this.config.accessTokenExpirationDays),
);
await this.jwtTokenRepository.saveJwt(
refresh_token,
this.datePlusDays(this.config.refreshTokenExpirationDays),
);
return { access_token, refresh_token };
}
private generateRefreshToken(userId: string): string {
return jwt.sign({ sub: userId }, process.env.JWT_SECRET as string, { expiresIn: '180d' });
async logout(data: LogoutInputDomainInterface): Promise<void> {
await this.jwtTokenRepository.revokeJwt(data.access_token);
await this.jwtTokenRepository.revokeJwt(data.refresh_token);
}
async refreshAccessToken(refreshToken: string): Promise<CompleteLoginResponseDomainInterface> {
async refreshAccessToken(refreshToken: string) {
const isValid = await this.jwtTokenRepository.isJwtValid(refreshToken);
if (!isValid)
throw new BadRequestException('Invalid or revoked refresh token');
try {
const payload = jwt.verify(refreshToken, process.env.JWT_SECRET as string) as any;
const userId = payload.sub;
const payload = jwt.verify(refreshToken, this.config.jwtSecret) as any;
const user_id = payload.sub;
const access_token = this.generateAccessToken(userId);
const newRefreshToken = this.generateRefreshToken(userId);
const access_token = this.generateAccessToken(user_id);
const newRefreshToken = this.generateRefreshToken(user_id);
await this.jwtTokenRepository.saveJwt(
access_token,
this.datePlusDays(this.config.accessTokenExpirationDays),
);
await this.jwtTokenRepository.saveJwt(
newRefreshToken,
this.datePlusDays(this.config.refreshTokenExpirationDays),
);
return { access_token, refresh_token: newRefreshToken };
} catch (error) {
throw new Error('Invalid refresh token');
throw new BadRequestException('Invalid refresh token');
}
}
async revokeJwt(jwtToken: string) {
await this.jwtTokenRepository.revokeJwt(jwtToken);
}
async revokeUuid(uuid: string) {
await this.uuidTokenRepository.revokeUuid(uuid);
}
private generateAccessToken(user_id: string): string {
return jwt.sign({ sub: user_id }, this.config.jwtSecret, {
expiresIn: `${this.config.accessTokenExpirationDays}d`,
});
}
private generateRefreshToken(user_id: string): string {
return jwt.sign({ sub: user_id }, this.config.jwtSecret, {
expiresIn: `${this.config.refreshTokenExpirationDays}d`,
});
}
private datePlusDays(days: number): Date {
return new Date(Date.now() + days * 24 * 60 * 60 * 1000);
}
}
@@ -4,13 +4,14 @@ import { APIClient } from '@wharfkit/antelope';
import { Table } from '@wharfkit/contract';
import type { BlockchainAccountInterface, IndexPosition } from './blockchain.types';
import { PublicKey } from '@wharfkit/antelope';
import { ConfigService } from '../config/config.service';
@Injectable()
export class BlockchainService {
private readonly apiClient: APIClient;
constructor() {
this.apiClient = new APIClient({ url: process.env.CHAIN_RPC });
constructor(private readonly config: ConfigService) {
this.apiClient = new APIClient({ url: this.config.chainRpc });
}
public async getAccountPublicKey(username: string): Promise<string> {
@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Global()
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}
@@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { envVars } from './env.validation';
@Injectable()
export class ConfigService {
public readonly jwtSecret = envVars.JWT_SECRET;
public readonly uuidRegExpirationMs = envVars.UUID_REG_EXPIRATION_MS;
public readonly uuidLoginExpirationMs = envVars.UUID_LOGIN_EXPIRATION_MS;
public readonly accessTokenExpirationDays =
envVars.ACCESS_TOKEN_EXPIRATION_DAYS;
public readonly refreshTokenExpirationDays =
envVars.REFRESH_TOKEN_EXPIRATION_DAYS;
// public readonly redisHost = envVars.REDIS_HOST;
// public readonly redisPort = envVars.REDIS_PORT;
// public readonly redisPassword = envVars.REDIS_PASSWORD;
// Database
public readonly dbType = envVars.DB_TYPE;
public readonly dbHost = envVars.POSTGRES_HOST;
public readonly dbPort = envVars.POSTGRES_PORT;
public readonly dbUsername = envVars.POSTGRES_USER;
public readonly dbPassword = envVars.POSTGRES_PASSWORD;
public readonly dbDatabase = envVars.POSTGRES_DB;
public readonly chainRpc = envVars.CHAIN_RPC;
public readonly chainId = envVars.CHAIN_ID;
}
@@ -0,0 +1,58 @@
import { config as dotenvConfig } from 'dotenv';
import { z } from 'zod';
// Грузим .env из process.cwd()
dotenvConfig();
const envSchema = z.object({
// JWT
JWT_SECRET: z.string().nonempty(),
UUID_REG_EXPIRATION_MS: z
.string()
.default('10000')
.transform((val) => parseInt(val, 10)),
UUID_LOGIN_EXPIRATION_MS: z
.string()
.default('10000')
.transform((val) => parseInt(val, 10)),
ACCESS_TOKEN_EXPIRATION_DAYS: z
.string()
.default('30')
.transform((val) => parseInt(val, 10)),
REFRESH_TOKEN_EXPIRATION_DAYS: z
.string()
.default('180')
.transform((val) => parseInt(val, 10)),
// Redis
// REDIS_HOST: z.string().default('127.0.0.1'),
// REDIS_PORT: z
// .string()
// .default('6379')
// .transform((val) => parseInt(val, 10)),
// REDIS_PASSWORD: z.string().default(''),
// Database
DB_TYPE: z.string().default('postgres'),
POSTGRES_HOST: z.string().default('127.0.0.1'),
POSTGRES_PORT: z
.string()
.default('5432')
.transform((val) => parseInt(val, 10)),
POSTGRES_USER: z.string().default('postgres'),
POSTGRES_PASSWORD: z.string().default('postgres'),
POSTGRES_DB: z.string().default('cardcoop'),
// Chain
CHAIN_RPC: z.string(),
CHAIN_ID: z.string(),
});
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error('❌ Environment validation error:', parsed.error.format());
process.exit(1);
}
export const envVars = parsed.data;
@@ -1,26 +1,35 @@
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { AccessRequest } from '../../../domain/entities/access-request.entity';
import { AccessRequestORM } from '../entities/access-request.orm-entity';
import { InjectRepository } from '@nestjs/typeorm';
import { AccessRequestRepository } from '../../../domain/repositories/access-request.repository';
import { AccessRequest } from 'src/domain/entities/access-request.entity';
@Injectable()
export class AccessRequestRepositoryImpl implements AccessRequestRepository {
constructor(
@InjectRepository(AccessRequest)
private readonly repository: Repository<AccessRequest>,
@InjectRepository(AccessRequestORM)
private readonly repository: Repository<AccessRequestORM>,
) {}
async save(request: AccessRequest ): Promise<AccessRequest> {
return this.repository.save(request);
async save(request: AccessRequest): Promise<AccessRequest> {
const result = await this.repository.save(request);
return new AccessRequest(result)
}
async findByTicket(ticket: string): Promise<AccessRequest | null> {
return this.repository.findOne({ where: { ticket } });
const request = await this.repository.findOne({ where: { ticket } });
if (request)
return new AccessRequest(request)
else return null
}
async findByUsernameAndCoopName(username: string, coopname: string): Promise<AccessRequest | null> {
return this.repository.findOne({ where: { username, coopname } });
const result = await this.repository.findOne({ where: { username, coopname } });
if (result)
return new AccessRequest(result)
else return null
}
async deleteByUsernameAndCoopName(username: string, coopname: string): Promise<void> {
@@ -28,6 +37,7 @@ export class AccessRequestRepositoryImpl implements AccessRequestRepository {
}
async findByUsername(username: string): Promise<AccessRequest[]> {
return this.repository.find({ where: { username } });
const result = await this.repository.find({ where: { username } });
return result.map((item) => new AccessRequest(item));
}
}
@@ -1,32 +1,103 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { Card } from '../../../domain/entities/card.entity';
import { CardORM } from '../entities/card.orm-entity';
import { InjectRepository } from '@nestjs/typeorm';
import { CardRepository } from '../../../domain/repositories/card.repository';
import { Card } from 'src/domain/entities/card.entity';
/**
* Реализация репозитория карт пайщика
*/
@Injectable()
export class CardRepositoryImpl implements CardRepository {
constructor(
@InjectRepository(Card)
private readonly repository: Repository<Card>,
@InjectRepository(CardORM)
private readonly repository: Repository<CardORM>,
) {}
/**
* Сохраняет карту в БД
* @param card Данные карты (частичные)
* @returns Сохраненная карта
*/
async save(card: Partial<Card>): Promise<Card> {
return this.repository.save(card);
return new Card(await this.repository.save(card));
}
async findByUsername(username: string): Promise<Card | null> {
if (!username) {
throw new BadRequestException('Username must be provided');
/**
* Ищет карту по ID
* @param id ID карты
* @returns Карта или null
*/
async findById(id: string): Promise<Card | null> {
if (!id) {
throw new BadRequestException('ID must be provided');
}
return this.repository.findOne({ where: { username } });
const result = await this.repository.findOne({ where: { id } });
return result ? new Card(result) : null;
}
async findByUsernameAndUserId(username: string, userId: string): Promise<Card | null> {
if (!username || userId) {
throw new BadRequestException('Username and userId must be provided');
/**
* Ищет карту по имени пользователя и названию кооператива
* @param username Имя пользователя
* @param coop_name Название кооператива
* @returns Карта или null
*/
async findByUsername(
username: string,
coop_name: string,
): Promise<Card | null> {
if (!username || !coop_name) {
throw new BadRequestException('Username and coop_name must be provided');
}
return this.repository.findOne({ where: { username, userId } });
const result = await this.repository.findOne({
where: { username, coop_name },
});
return result ? new Card(result) : null;
}
/**
* Ищет все карты пользователя
* @param user_id ID пользователя
* @returns Массив карт
*/
async findByUserId(user_id: string): Promise<Card[]> {
if (!user_id) {
throw new BadRequestException('User ID must be provided');
}
const cards = await this.repository.find({ where: { user_id } });
return cards.map((card) => new Card(card));
}
/**
* Ищет карту пользователя для конкретного кооператива
* @param user_id ID пользователя
* @param coop_name Название кооператива
* @returns Карта или null
*/
async findByUserIdAndCoopName(
user_id: string,
coop_name: string,
): Promise<Card | null> {
if (!user_id || !coop_name) {
throw new BadRequestException('User ID and coop_name must be provided');
}
const card = await this.repository.findOne({
where: { user_id, coop_name },
});
return card ? new Card(card) : null;
}
/**
* Ищет все карты, связанные с конкретными приватными данными
* @param private_data_id ID приватных данных
* @returns Массив карт
*/
async findByPrivateDataId(private_data_id: string): Promise<Card[]> {
if (!private_data_id) {
throw new BadRequestException('Private data ID must be provided');
}
const cards = await this.repository.find({ where: { private_data_id } });
return cards.map((card) => new Card(card));
}
}
@@ -1,17 +1,21 @@
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { Coop } from '../../../domain/entities/coop.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { CoopRepository } from '../../../domain/repositories/coop.repository';
import { Coop } from 'src/domain/entities/coop.entity';
import { CoopORM } from '../entities/coop.orm-entity';
@Injectable()
export class CoopRepositoryImpl implements CoopRepository {
constructor(
@InjectRepository(Coop)
private readonly repository: Repository<Coop>,
@InjectRepository(CoopORM)
private readonly repository: Repository<CoopORM>,
) {}
async findByName(coopname: string): Promise<Coop | null> {
return this.repository.findOne({ where: { coopname } });
const result = await this.repository.findOne({ where: { coopname } });
if (result)
return new Coop(result)
else return null
}
}
@@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { JwtTokenRepository } from 'src/domain/repositories/jwt-token.repository';
import { Repository, LessThan } from 'typeorm';
import { JwtTokenORM } from '../entities/jwt-token.entity';
@Injectable()
export class TypeormJwtTokenRepository implements JwtTokenRepository {
constructor(
@InjectRepository(JwtTokenORM)
private readonly repo: Repository<JwtTokenORM>
) {}
async saveJwt(jwt: string, expires_at: Date): Promise<void> {
const token = this.repo.create({ jwt, expires_at });
await this.repo.save(token);
}
async isJwtValid(jwt: string): Promise<boolean> {
const token = await this.repo.findOne({ where: { jwt } });
if (!token) return false;
return token.expires_at > new Date();
}
async revokeJwt(jwt: string): Promise<void> {
await this.repo.delete({ jwt });
}
async cleanupExpired(): Promise<void> {
await this.repo.delete({ expires_at: LessThan(new Date()) });
}
}
@@ -0,0 +1,62 @@
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { PrivateDataORM } from '../entities/private-data.orm-entity';
import { PrivateDataRepository } from '../../../domain/repositories/private-data.repository';
import { PrivateData } from 'src/domain/entities/private-data.entity';
/**
* Реализация репозитория для работы с приватными данными
*/
@Injectable()
export class PrivateDataRepositoryImpl implements PrivateDataRepository {
constructor(
@InjectRepository(PrivateDataORM)
private readonly repository: Repository<PrivateDataORM>,
) {}
/**
* Сохраняет приватные данные в БД
* @param data Объект приватных данных (частичный)
* @returns Сохраненные приватные данные
*/
async save(data: Partial<PrivateData>): Promise<PrivateData> {
const savedData = await this.repository.save(data);
return new PrivateData(savedData);
}
/**
* Ищет приватные данные по ID
* @param id ID приватных данных
* @returns Объект приватных данных или null
*/
async findById(id: string): Promise<PrivateData | null> {
const result = await this.repository.findOne({ where: { id } });
return result ? new PrivateData(result) : null;
}
/**
* Ищет приватные данные по ID пользователя
* @param user_id ID пользователя
* @returns Объект приватных данных или null
*/
async findByUserId(user_id: string): Promise<PrivateData | null> {
const result = await this.repository.findOne({ where: { user_id } });
return result ? new PrivateData(result) : null;
}
/**
* Обновляет приватные данные
* @param id ID приватных данных
* @param data Обновляемые поля
* @returns Обновленный объект приватных данных
*/
async update(id: string, data: Partial<PrivateData>): Promise<PrivateData> {
await this.repository.update(id, data);
const updated = await this.repository.findOne({ where: { id } });
if (!updated) {
throw new Error(`Private data with id ${id} not found after update`);
}
return new PrivateData(updated);
}
}
@@ -1,25 +0,0 @@
import { Token } from 'src/domain/entities/token.entity';
import type { TokenRepository } from 'src/domain/repositories/token.repository';
import type { Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
@Injectable()
export class TokenRepositoryImpl implements TokenRepository {
constructor(
@InjectRepository(Token)
private readonly repository: Repository<Token>,
) {}
async create(Token: Token): Promise<Token> {
return this.repository.save(Token);
}
async findByTicket(ticket: string): Promise<Token | null> {
return this.repository.findOne({ where: { ticket } });
}
async deleteByTicket(ticket: string): Promise<void> {
await this.repository.delete({ ticket });
}
}
@@ -1,29 +1,38 @@
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from '../../../domain/entities/user.entity';
import { UserRepository } from '../../../domain/repositories/user.repository';
import { UserORM } from '../entities/user.entity';
import { User } from 'src/domain/entities/user.entity';
@Injectable()
export class UserRepositoryImpl implements UserRepository {
constructor(
@InjectRepository(User)
private readonly repository: Repository<User>,
@InjectRepository(UserORM)
private readonly repository: Repository<UserORM>,
) {}
async create(user: Partial<User>): Promise<User> {
return this.repository.save(user);
const result = await this.repository.save(user);
return new User(result)
}
async findByEmail(email: string): Promise<User | null> {
return this.repository.findOne({ where: { email } });
const result = await this.repository.findOne({ where: { email } });
if (result)
return new User(result)
else return null
}
async findById(id: string): Promise<User | null> {
return this.repository.findOne({ where: { id } });
const result = await this.repository.findOne({ where: { id } });
if (result)
return new User(result)
else return null
}
async update(user: User): Promise<User> {
return this.repository.save(user);
const result = await this.repository.save(user);
return new User(result)
}
}
@@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, LessThan } from 'typeorm';
import { UuidTokenRepository } from 'src/domain/repositories/uuid-token.repository';
import { UuidTokenORM } from 'src/infrastructure/database/entities/uuid-token.entity';
@Injectable()
export class TypeormUuidTokenRepository implements UuidTokenRepository {
constructor(
@InjectRepository(UuidTokenORM)
private readonly repo: Repository<UuidTokenORM>
) {}
async saveUuid(value: string, expires_at: Date): Promise<void> {
const token = this.repo.create({ value, expires_at });
await this.repo.save(token);
}
async isUuidValid(value: string): Promise<boolean> {
const token = await this.repo.findOne({ where: { value } });
if (!token) return false;
return token.expires_at > new Date();
}
async revokeUuid(value: string): Promise<void> {
await this.repo.delete({ value });
}
async cleanupExpired(): Promise<void> {
await this.repo.delete({ expires_at: LessThan(new Date()) });
}
}
@@ -1,45 +1,62 @@
import { Global, Module } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../../domain/entities/user.entity';
import { Card } from '../../domain/entities/card.entity';
import { AccessRequest } from '../../domain/entities/access-request.entity';
import { Coop } from '../../domain/entities/coop.entity';
import { UserORM } from './entities/user.entity';
import { CardORM } from './entities/card.orm-entity';
import { CoopORM } from './entities/coop.orm-entity';
import { UserRepositoryImpl } from './adapters/user.repository.impl';
import { CardRepositoryImpl } from './adapters/card.repository.impl';
import { AccessRequestRepositoryImpl } from './adapters/access-request.repository.impl';
import { CoopRepositoryImpl } from './adapters/coop.repository.impl';
import { USER_REPOSITORY } from '../../domain/repositories/user.repository';
import { CARD_REPOSITORY } from '../../domain/repositories/card.repository';
import { ACCESS_REQUEST_REPOSITORY } from '../../domain/repositories/access-request.repository';
import { COOP_REPOSITORY } from '../../domain/repositories/coop.repository';
import { Token } from 'src/domain/entities/token.entity';
import { TOKEN_REPOSITORY } from 'src/domain/repositories/token.repository';
import { TokenRepositoryImpl } from './adapters/token.repository.impl';
import { UUID_TOKEN_REPOSITORY } from 'src/domain/repositories/uuid-token.repository';
import { JWT_TOKEN_REPOSITORY } from 'src/domain/repositories/jwt-token.repository';
import { TypeormUuidTokenRepository } from './adapters/uuid-token.repository.impl';
import { TypeormJwtTokenRepository } from './adapters/jwt-token.repository.impl';
import { UuidTokenORM } from 'src/infrastructure/database/entities/uuid-token.entity';
import { JwtTokenORM } from 'src/infrastructure/database/entities/jwt-token.entity';
import { AccessRequestORM } from './entities/access-request.orm-entity';
import { AccessRequestRepositoryImpl } from './adapters/access-request.repository.impl';
import { ConfigService } from '../config/config.service';
import { ConfigModule } from '../config/config.module';
import { PrivateDataORM } from './entities/private-data.orm-entity';
import { PrivateDataRepositoryImpl } from './adapters/private-data.repository.impl';
import { PRIVATE_DATA_REPOSITORY } from 'src/domain/repositories/private-data.repository';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: '127.0.0.1',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'cardcoop',
synchronize: true,
entities: [
User,
Card,
AccessRequest,
Coop,
Token
],
ConfigModule,
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.dbHost,
port: configService.dbPort,
username: configService.dbUsername,
password: configService.dbPassword,
database: configService.dbDatabase,
synchronize: true,
entities: [
UserORM,
CardORM,
AccessRequestORM,
CoopORM,
UuidTokenORM,
JwtTokenORM,
PrivateDataORM,
],
}),
}),
TypeOrmModule.forFeature([
User,
Card,
AccessRequest,
Coop,
Token
UserORM,
CardORM,
AccessRequestORM,
CoopORM,
UuidTokenORM,
JwtTokenORM,
PrivateDataORM,
]),
],
providers: [
@@ -60,10 +77,17 @@ import { TokenRepositoryImpl } from './adapters/token.repository.impl';
useClass: CoopRepositoryImpl,
},
{
provide: TOKEN_REPOSITORY,
useClass: TokenRepositoryImpl,
provide: UUID_TOKEN_REPOSITORY,
useClass: TypeormUuidTokenRepository,
},
{
provide: JWT_TOKEN_REPOSITORY,
useClass: TypeormJwtTokenRepository,
},
{
provide: PRIVATE_DATA_REPOSITORY,
useClass: PrivateDataRepositoryImpl,
},
],
exports: [
TypeOrmModule,
@@ -71,7 +95,9 @@ import { TokenRepositoryImpl } from './adapters/token.repository.impl';
CARD_REPOSITORY,
ACCESS_REQUEST_REPOSITORY,
COOP_REPOSITORY,
TOKEN_REPOSITORY
UUID_TOKEN_REPOSITORY,
JWT_TOKEN_REPOSITORY,
PRIVATE_DATA_REPOSITORY,
],
})
export class DatabaseModule {}
@@ -0,0 +1,30 @@
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import type { AccessMetaDomainInterface } from '../../../domain/interfaces/access-meta.interface';
@Entity()
export class AccessRequestORM {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ unique: true })
ticket!: string;
@Column()
username!: string;
@Column()
coopname!: string;
@Column()
encrypted_data!: string;
@Column()
public_key!: string;
@Column({default: false})
ticket_is_used!: boolean;
@Column('simple-json') // Поле `meta` хранится как JSON
meta!: AccessMetaDomainInterface;
}
@@ -0,0 +1,43 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import type { CardMetaDomainInterface } from '../../../domain/interfaces/card-meta.interface';
import { PrivateDataORM } from './private-data.orm-entity';
/**
* ORM-сущность карты пайщика для хранения в БД
* Связана с приватными данными через private_data_id
*/
@Entity('card')
export class CardORM {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
user_id!: string;
@Column()
private_data_id!: string;
@Column()
username!: string;
@Column()
coop_name!: string;
@Column()
encrypted_key!: string;
// Устанавливаем связь с приватными данными
@ManyToOne(() => PrivateDataORM)
@JoinColumn({ name: 'private_data_id' })
privateData!: PrivateDataORM;
// Сохраняем meta как JSON-объект
@Column('simple-json')
meta!: CardMetaDomainInterface;
}
@@ -0,0 +1,14 @@
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class CoopORM {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ unique: true })
coopname!: string;
@Column()
public_key!: string;
}
@@ -0,0 +1,14 @@
import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm';
@Entity('jwt_tokens')
export class JwtTokenORM {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Index()
@Column({ type: 'text', unique: true })
jwt!: string;
@Column({ type: 'timestamp' })
expires_at!: Date;
}
@@ -0,0 +1,24 @@
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import type { PrivateDataMetaInterface } from '../../../domain/interfaces/private-data-meta.interface';
/**
* ORM-сущность приватных данных пользователя для хранения в БД
*/
@Entity('private_data')
export class PrivateDataORM {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
user_id!: string;
@Column('text')
encrypted_data!: string;
@Column()
data_hash!: string;
// Сохраняем meta как JSON-объект
@Column('simple-json')
meta!: PrivateDataMetaInterface;
}
@@ -0,0 +1,16 @@
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity('user')
export class UserORM {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ unique: true })
email!: string;
@Column()
salt!: string;
@Column()
hash_key!: string;
}
@@ -0,0 +1,14 @@
import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm';
@Entity('uuid_tokens')
export class UuidTokenORM {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Index()
@Column({ type: 'text', unique: true })
value!: string;
@Column({ type: 'timestamp' })
expires_at!: Date;
}
@@ -1,15 +1,18 @@
import { Module } from "@nestjs/common";
import { BlockchainModule } from "./blockchain/blockchain.module";
import { DatabaseModule } from "./database/database.module";
import { ConfigModule } from "./config/config.module";
@Module({
imports: [
BlockchainModule,
DatabaseModule
DatabaseModule,
ConfigModule
],
exports: [
BlockchainModule,
DatabaseModule
DatabaseModule,
ConfigModule
],
})
+251 -12
View File
@@ -170,16 +170,17 @@
"post": {
"description": "Выход",
"operationId": "logout",
"parameters": [
{
"name": "Authorization",
"required": true,
"in": "header",
"schema": {
"type": "string"
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LogoutInputDTO"
}
}
}
],
},
"responses": {
"200": {
"description": "Успешный выход"
@@ -226,7 +227,8 @@
},
"/card/schema": {
"get": {
"operationId": "CardController_getSchema",
"description": "Извлечение актуальной схемы карты",
"operationId": "getSchema",
"parameters": [],
"responses": {
"200": {
@@ -240,6 +242,99 @@
}
}
},
"summary": "getSchema",
"tags": [
"Card"
]
}
},
"/card/user": {
"get": {
"description": "Получение всех карт пользователя",
"operationId": "getUserCards",
"parameters": [],
"responses": {
"200": {
"description": "Список карт пользователя",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GetUserCardsResponseDTO"
}
}
}
}
}
},
"summary": "getUserCards",
"tags": [
"Card"
]
}
},
"/card/{card_id}/private-data": {
"get": {
"description": "Получение приватных данных по ID карты",
"operationId": "getPrivateData",
"parameters": [
{
"name": "card_id",
"required": true,
"in": "path",
"description": "ID карты",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Приватные данные пользователя",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PrivateDataResponseDTO"
}
}
}
}
},
"summary": "getPrivateData",
"tags": [
"Card"
]
}
},
"/card/{card_id}": {
"delete": {
"description": "Деактивация карты",
"operationId": "deactivateCard",
"parameters": [
{
"name": "card_id",
"required": true,
"in": "path",
"description": "ID карты для деактивации",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Карта успешно деактивирована",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IssueCardResponseDTO"
}
}
}
}
},
"summary": "deactivateCard",
"tags": [
"Card"
]
@@ -520,6 +615,25 @@
"refresh_token"
]
},
"LogoutInputDTO": {
"type": "object",
"properties": {
"access_token": {
"type": "string",
"description": "Access-токен для удаления",
"example": "accesstoken123"
},
"refresh_token": {
"type": "string",
"description": "Refresh-токен для удаления",
"example": "refreshtoken123"
}
},
"required": [
"access_token",
"refresh_token"
]
},
"CardMetaDTO": {
"type": "object",
"properties": {
@@ -527,10 +641,28 @@
"type": "number",
"description": "Версия модели данных карты",
"example": 1
},
"card_type": {
"type": "string",
"description": "Тип карты",
"example": "standard"
},
"valid_until": {
"format": "date-time",
"type": "string",
"description": "Дата окончания действия карты",
"example": "2030-01-01"
},
"is_active": {
"type": "boolean",
"description": "Активна ли карта",
"example": true,
"default": true
}
},
"required": [
"version"
"version",
"is_active"
]
},
"IssueCardInputDTO": {
@@ -541,6 +673,11 @@
"description": "Имя пользователя, которому будет выдана карта",
"example": "user123"
},
"coop_name": {
"type": "string",
"description": "Название кооператива",
"example": "coop_example"
},
"encrypted_key": {
"type": "string",
"description": "Зашифрованный приватный ключ",
@@ -554,7 +691,8 @@
"meta": {
"description": "Метаданные, связанные с картой",
"example": {
"version": 1
"version": 1,
"card_type": "standard"
},
"allOf": [
{
@@ -565,6 +703,7 @@
},
"required": [
"username",
"coop_name",
"encrypted_key",
"encrypted_data",
"meta"
@@ -582,11 +721,36 @@
"type": "string",
"description": "Имя пользователя, связанного с картой",
"example": "username123"
},
"coop_name": {
"type": "string",
"description": "Название кооператива",
"example": "coop_example"
},
"issued_at": {
"format": "date-time",
"type": "string",
"description": "Дата выпуска карты",
"example": "2023-01-01T12:00:00Z"
},
"card_type": {
"type": "string",
"description": "Тип карты",
"example": "standard"
},
"is_active": {
"type": "boolean",
"description": "Активна ли карта",
"example": true
}
},
"required": [
"id",
"username"
"username",
"coop_name",
"issued_at",
"card_type",
"is_active"
]
},
"SchemaItem": {
@@ -633,6 +797,81 @@
"oneOf"
]
},
"GetUserCardsResponseDTO": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Уникальный идентификатор карты",
"example": "123e4567-e89b-12d3-a456-426614174000"
},
"username": {
"type": "string",
"description": "Имя пользователя в кооперативе",
"example": "username123"
},
"coop_name": {
"type": "string",
"description": "Название кооператива",
"example": "coop_example"
},
"issued_at": {
"format": "date-time",
"type": "string",
"description": "Дата выпуска карты",
"example": "2023-01-01T12:00:00Z"
},
"card_type": {
"type": "string",
"description": "Тип карты",
"example": "standard"
},
"is_active": {
"type": "boolean",
"description": "Активна ли карта",
"example": true
}
},
"required": [
"id",
"username",
"coop_name",
"issued_at",
"card_type",
"is_active"
]
},
"PrivateDataResponseDTO": {
"type": "object",
"properties": {
"encrypted_data": {
"type": "string",
"description": "Зашифрованные данные пользователя",
"example": "encrypted-data-string"
},
"data_hash": {
"type": "string",
"description": "Хеш данных для верификации",
"example": "data-hash-string"
},
"version": {
"type": "number",
"description": "Версия формата данных",
"example": 1
},
"data_type": {
"type": "string",
"description": "Тип приватных данных",
"example": "personal"
}
},
"required": [
"encrypted_data",
"data_hash",
"version",
"data_type"
]
},
"PrepareShareDataInputDTO": {
"type": "object",
"properties": {
@@ -0,0 +1,256 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { JwtService } from '@nestjs/jwt';
import { getRepositoryToken } from '@nestjs/typeorm';
import { UserORM } from '../src/infrastructure/database/entities/user.entity';
import { CardORM } from '../src/infrastructure/database/entities/card.orm-entity';
import { Repository } from 'typeorm';
import { ConfigService } from '../src/infrastructure/config/config.service';
import { v4 } from 'uuid';
import { PrivateDataORM } from '../src/infrastructure/database/entities/private-data.orm-entity';
import { CardMetaDTO } from '../src/application/dto/card-meta.dto';
import { AccessRequestORM } from '../src/infrastructure/database/entities/access-request.orm-entity';
import { BlockchainService } from '../src/infrastructure/blockchain/blockchain.service';
/**
* Тесты для контроллера доступа
* Проверяет подготовку к обмену данными, обмен данными и получение данных кооперативом
*/
describe('AccessController (e2e)', () => {
let app: INestApplication;
let jwtService: JwtService;
let userRepository: Repository<UserORM>;
let cardRepository: Repository<CardORM>;
let privateDataRepository: Repository<PrivateDataORM>;
let accessRequestRepository: Repository<AccessRequestORM>;
let configService: ConfigService;
let blockchainService: BlockchainService;
const testUser = {
email: `test-${v4()}@example.com`,
password: 'password123',
id: v4(),
};
const testCoop = {
name: `coop-${v4()}`,
public_key: 'EOS8mUftJXepGzdQ2TaCduNuSPAfXJHf22uex4u41ab1EVv9EAhWt',
announce: 'Test Cooperative',
};
// Данные для тестовой карты
const testCard = {
username: `user-${v4()}`,
coop_name: testCoop.name,
encrypted_key: 'encrypted-key-data',
encrypted_data: 'encrypted-personal-data',
meta: {
version: 1,
card_type: 'standard',
} as CardMetaDTO,
};
let accessToken: string;
let cardId: string;
let ticket: string;
let coopAccessToken: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
jwtService = moduleFixture.get<JwtService>(JwtService);
configService = moduleFixture.get<ConfigService>(ConfigService);
userRepository = moduleFixture.get<Repository<UserORM>>(
getRepositoryToken(UserORM),
);
cardRepository = moduleFixture.get<Repository<CardORM>>(
getRepositoryToken(CardORM),
);
privateDataRepository = moduleFixture.get<Repository<PrivateDataORM>>(
getRepositoryToken(PrivateDataORM),
);
accessRequestRepository = moduleFixture.get<Repository<AccessRequestORM>>(
getRepositoryToken(AccessRequestORM),
);
blockchainService = moduleFixture.get<BlockchainService>(BlockchainService);
await app.init();
// Создаем тестового пользователя
const user = userRepository.create({
id: testUser.id,
email: testUser.email,
salt: 'test-salt',
hash_key: 'test-hash-key',
});
await userRepository.save(user);
// Генерируем JWT токен для аутентификации
accessToken = jwtService.sign(
{ user_id: testUser.id, email: testUser.email },
{ secret: configService.jwtSecret, expiresIn: '1h' },
);
// Заглушка для BlockchainService
jest
.spyOn(blockchainService, 'getAccountPublicKey')
.mockImplementation(async (name: string) =>
Promise.resolve(testCoop.public_key),
);
jest.spyOn(blockchainService, 'getSingleRow').mockImplementation(async () =>
Promise.resolve({
announce: testCoop.announce,
}),
);
// Выпускаем тестовую карту
const cardResponse = await request(app.getHttpServer())
.post('/card/issue')
.set('Authorization', `Bearer ${accessToken}`)
.send(testCard)
.expect(201);
cardId = cardResponse.body.id;
});
afterAll(async () => {
// Очистка тестовых данных
await accessRequestRepository.delete({ username: testCard.username });
await cardRepository.delete({ user_id: testUser.id });
await privateDataRepository.delete({ user_id: testUser.id });
await userRepository.delete({ id: testUser.id });
await app.close();
});
/**
* Тесты процесса предоставления доступа
*/
describe('Предоставление доступа', () => {
it('POST /access/prepare-share-data должен подготовить данные для обмена', async () => {
const response = await request(app.getHttpServer())
.post('/access/prepare-share-data')
.set('Authorization', `Bearer ${accessToken}`)
.send({ coopname: testCoop.name })
.expect(201);
expect(response.body).toHaveProperty('coopname');
expect(response.body).toHaveProperty('public_key');
expect(response.body).toHaveProperty('announce');
expect(response.body.coopname).toBe(testCoop.name);
expect(response.body.public_key).toBe(testCoop.public_key);
expect(response.body.announce).toBe(testCoop.announce);
});
it('POST /access/share-data должен создать запрос на доступ', async () => {
const shareData = {
username: testCard.username,
coopname: testCoop.name,
encrypted_data: 'encrypted-data-for-coop',
public_key: testCoop.public_key,
meta: {
additional: 'info',
},
};
const response = await request(app.getHttpServer())
.post('/access/share-data')
.set('Authorization', `Bearer ${accessToken}`)
.send(shareData)
.expect(201);
expect(response.body).toHaveProperty('ticket');
ticket = response.body.ticket;
// Проверяем, что запрос на доступ создан в БД
const accessRequest = await accessRequestRepository.findOne({
where: { ticket },
});
expect(accessRequest).toBeDefined();
if (accessRequest) {
expect(accessRequest.username).toBe(shareData.username);
expect(accessRequest.coopname).toBe(shareData.coopname);
expect(accessRequest.encrypted_data).toBe(shareData.encrypted_data);
expect(accessRequest.ticket_is_used).toBe(false);
}
});
});
/**
* Тесты обмена тикета на JWT и получения данных
*/
describe('Обмен тикета и получение данных', () => {
it('POST /access/exchange-ticket должен обменять тикет на токен доступа', async () => {
const response = await request(app.getHttpServer())
.post('/access/exchange-ticket')
.send({ ticket })
.expect(201);
expect(response.body).toHaveProperty('access_token');
coopAccessToken = response.body.access_token;
// Проверяем, что тикет помечен как использованный
const accessRequest = await accessRequestRepository.findOne({
where: { ticket },
});
expect(accessRequest).toBeDefined();
if (accessRequest) {
expect(accessRequest.ticket_is_used).toBe(true);
}
});
it('GET /access/get-encrypted-data/:username/:coopname должен вернуть зашифрованные данные', async () => {
const response = await request(app.getHttpServer())
.get(`/access/get-encrypted-data/${testCard.username}/${testCoop.name}`)
.set('Authorization', `Bearer ${coopAccessToken}`)
.expect(200);
expect(response.body).toHaveProperty('coopname');
expect(response.body).toHaveProperty('username');
expect(response.body).toHaveProperty('encrypted_data');
expect(response.body).toHaveProperty('meta');
expect(response.body).toHaveProperty('public_key');
expect(response.body.coopname).toBe(testCoop.name);
expect(response.body.username).toBe(testCard.username);
expect(response.body.public_key).toBe(testCoop.public_key);
});
});
/**
* Тесты на проверку безопасности и ошибки
*/
describe('Проверки безопасности', () => {
it('POST /access/prepare-share-data без авторизации должен вернуть ошибку 401', async () => {
await request(app.getHttpServer())
.post('/access/prepare-share-data')
.send({ coopname: testCoop.name })
.expect(401);
});
it('GET /access/get-encrypted-data/:username/:coopname с неверным токеном должен вернуть ошибку 401', async () => {
await request(app.getHttpServer())
.get(`/access/get-encrypted-data/${testCard.username}/${testCoop.name}`)
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
it('POST /access/exchange-ticket с неверным тикетом должен вернуть ошибку', async () => {
await request(app.getHttpServer())
.post('/access/exchange-ticket')
.send({ ticket: 'invalid-ticket' })
.expect(400);
});
it('POST /access/exchange-ticket с уже использованным тикетом должен вернуть ошибку', async () => {
await request(app.getHttpServer())
.post('/access/exchange-ticket')
.send({ ticket })
.expect(400);
});
});
});
@@ -0,0 +1,11 @@
/**
* Этот файл запускает все e2e-тесты для контроллеров в нужном порядке
* Это позволяет обеспечить правильную очередность запуска тестов,
* что может быть важно из-за зависимостей между ними
*/
// Для правильного порядка запуска - сначала авторизация,
// потом карты, потом доступ (зависит от наличия карт)
import './auth.controller.spec';
import './card.controller.spec';
import './access.controller.spec';
+11 -4
View File
@@ -1,12 +1,12 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
import { AppModule } from '../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
@@ -15,10 +15,17 @@ describe('AppController (e2e)', () => {
await app.init();
});
it('/ (GET)', () => {
afterAll(async () => {
await app.close();
});
it('/ GET - проверка работоспособности приложения', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
.expect((res) => {
expect(res.body).toHaveProperty('status');
expect(res.body.status).toBe('ok');
});
});
});
@@ -0,0 +1,168 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { v4 } from 'uuid';
/**
* Тесты для контроллера авторизации
* Проверяет процессы регистрации, подтверждения email, входа в систему,
* обновления токена и выхода из системы
*/
describe('AuthController (e2e)', () => {
let app: INestApplication;
const testUser = {
email: `test-${v4()}@example.com`,
password: 'password123',
};
let accessToken: string;
let refreshToken: string;
let registrationUuid: string;
let verificationCode = '123456';
let salt: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
/**
* Тест процесса регистрации
*/
describe('Регистрация', () => {
it('POST /auth/initiate-registration должен начать процесс регистрации', async () => {
const response = await request(app.getHttpServer())
.post('/auth/initiate-registration')
.send({
email: testUser.email,
privacy_accepted: true,
})
.expect(201);
// Сохраняем UUID для использования в следующих тестах
expect(response.body).toHaveProperty('uuid');
expect(response.body).toHaveProperty('salt');
registrationUuid = response.body.uuid;
salt = response.body.salt;
});
it('POST /auth/verify-email должен подтвердить email', async () => {
const response = await request(app.getHttpServer())
.post('/auth/verify-email')
.send({
uuid: registrationUuid,
code: verificationCode,
email: testUser.email,
})
.expect(201);
expect(response.body).toHaveProperty('verified');
expect(response.body.verified).toBe(true);
});
it('POST /auth/complete-registration должен завершить регистрацию', async () => {
// Генерируем хеш ключ (в реальности это делает клиент)
const hashKey = 'hashed_password_example';
const response = await request(app.getHttpServer())
.post('/auth/complete-registration')
.send({
uuid: registrationUuid,
email: testUser.email,
hash_key: hashKey,
salt: salt, // Передаем соль явно
})
.expect(201);
expect(response.body).toHaveProperty('access_token');
expect(response.body).toHaveProperty('refresh_token');
accessToken = response.body.access_token;
refreshToken = response.body.refresh_token;
});
});
/**
* Тест процесса входа в систему
*/
describe('Вход в систему', () => {
it('POST /auth/initiate-login должен начать процесс входа', async () => {
const response = await request(app.getHttpServer())
.post('/auth/initiate-login')
.send({
email: testUser.email,
})
.expect(201);
expect(response.body).toHaveProperty('uuid');
expect(response.body).toHaveProperty('salt');
registrationUuid = response.body.uuid;
salt = response.body.salt;
});
it('POST /auth/complete-login должен завершить вход в систему', async () => {
// Генерируем хеш ключ (в реальности это делает клиент)
const hashKey = 'hashed_password_example';
const response = await request(app.getHttpServer())
.post('/auth/complete-login')
.send({
uuid: registrationUuid,
email: testUser.email,
hash_key: hashKey,
})
.expect(201);
expect(response.body).toHaveProperty('access_token');
expect(response.body).toHaveProperty('refresh_token');
accessToken = response.body.access_token;
refreshToken = response.body.refresh_token;
});
});
/**
* Тест обновления токена
*/
describe('Обновление токена', () => {
it('POST /auth/refresh-token должен обновить токены', async () => {
const response = await request(app.getHttpServer())
.post('/auth/refresh-token')
.send({
refresh_token: refreshToken,
})
.expect(201);
expect(response.body).toHaveProperty('access_token');
expect(response.body).toHaveProperty('refresh_token');
accessToken = response.body.access_token;
refreshToken = response.body.refresh_token;
});
});
/**
* Тест выхода из системы
*/
describe('Выход из системы', () => {
it('POST /auth/logout должен завершить сессию', async () => {
await request(app.getHttpServer())
.post('/auth/logout')
.set('Authorization', `Bearer ${accessToken}`)
.send({
refresh_token: refreshToken,
})
.expect(201);
});
});
});
@@ -0,0 +1,263 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { JwtService } from '@nestjs/jwt';
import { getRepositoryToken } from '@nestjs/typeorm';
import { UserORM } from '../src/infrastructure/database/entities/user.entity';
import { UuidTokenORM } from '../src/infrastructure/database/entities/uuid-token.entity';
import { CardORM } from '../src/infrastructure/database/entities/card.orm-entity';
import { Repository } from 'typeorm';
import { ConfigService } from '../src/infrastructure/config/config.service';
import { v4 } from 'uuid';
import { PrivateDataORM } from '../src/infrastructure/database/entities/private-data.orm-entity';
import { CardMetaDTO } from '../src/application/dto/card-meta.dto';
/**
* Тесты для контроллера карт
* Проверяет выпуск карты, получение списка карт,
* получение приватных данных и деактивацию карты
*/
describe('CardController (e2e)', () => {
let app: INestApplication;
let jwtService: JwtService;
let userRepository: Repository<UserORM>;
let cardRepository: Repository<CardORM>;
let privateDataRepository: Repository<PrivateDataORM>;
let configService: ConfigService;
const testUser = {
email: `test-${v4()}@example.com`,
password: 'password123',
id: v4(),
};
// Данные для тестовой карты
const testCard = {
username: `user-${v4()}`,
coop_name: `coop-${v4()}`,
encrypted_key: 'encrypted-key-data',
encrypted_data: 'encrypted-personal-data',
meta: {
version: 1,
card_type: 'standard',
} as CardMetaDTO,
};
let accessToken: string;
let cardId: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
jwtService = moduleFixture.get<JwtService>(JwtService);
configService = moduleFixture.get<ConfigService>(ConfigService);
userRepository = moduleFixture.get<Repository<UserORM>>(
getRepositoryToken(UserORM),
);
cardRepository = moduleFixture.get<Repository<CardORM>>(
getRepositoryToken(CardORM),
);
privateDataRepository = moduleFixture.get<Repository<PrivateDataORM>>(
getRepositoryToken(PrivateDataORM),
);
await app.init();
// Создаем тестового пользователя
const user = userRepository.create({
id: testUser.id,
email: testUser.email,
salt: 'test-salt',
hash_key: 'test-hash-key',
});
await userRepository.save(user);
// Генерируем JWT токен для аутентификации
accessToken = jwtService.sign(
{ user_id: testUser.id, email: testUser.email },
{ secret: configService.jwtSecret, expiresIn: '1h' },
);
});
afterAll(async () => {
// Очистка тестовых данных
await cardRepository.delete({ user_id: testUser.id });
await privateDataRepository.delete({ user_id: testUser.id });
await userRepository.delete({ id: testUser.id });
await app.close();
});
/**
* Тест получения схемы карты
*/
describe('Схема карты', () => {
it('GET /card/schema должен вернуть JSON-схему карты', async () => {
const response = await request(app.getHttpServer())
.get('/card/schema')
.expect(200);
expect(response.body).toBeDefined();
expect(response.body).toHaveProperty('$schema');
});
});
/**
* Тесты для выпуска и управления картами
*/
describe('Операции с картами', () => {
it('POST /card/issue должен выпустить новую карту', async () => {
const response = await request(app.getHttpServer())
.post('/card/issue')
.set('Authorization', `Bearer ${accessToken}`)
.send(testCard)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('username');
expect(response.body).toHaveProperty('coop_name');
expect(response.body.username).toBe(testCard.username);
expect(response.body.coop_name).toBe(testCard.coop_name);
expect(response.body.is_active).toBe(true);
cardId = response.body.id;
// Проверяем, что карта создана в БД
const card = await cardRepository.findOne({
where: { id: cardId },
});
expect(card).toBeDefined();
if (card) {
expect(card.username).toBe(testCard.username);
// Проверяем, что созданы приватные данные
const privateData = await privateDataRepository.findOne({
where: { user_id: testUser.id },
});
expect(privateData).toBeDefined();
if (privateData) {
expect(privateData.encrypted_data).toBe(testCard.encrypted_data);
// Проверяем связь карты с приватными данными
expect(card.private_data_id).toBe(privateData.id);
}
}
});
it('GET /card/user должен вернуть список карт пользователя', async () => {
const response = await request(app.getHttpServer())
.get('/card/user')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBeGreaterThanOrEqual(1);
const card = response.body.find((c) => c.id === cardId);
expect(card).toBeDefined();
expect(card.username).toBe(testCard.username);
expect(card.coop_name).toBe(testCard.coop_name);
expect(card.card_type).toBe(testCard.meta.card_type);
expect(card.is_active).toBe(true);
});
it('GET /card/:card_id/private-data должен вернуть приватные данные', async () => {
const response = await request(app.getHttpServer())
.get(`/card/${cardId}/private-data`)
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
expect(response.body).toHaveProperty('encrypted_data');
expect(response.body).toHaveProperty('data_hash');
expect(response.body).toHaveProperty('version');
expect(response.body).toHaveProperty('data_type');
expect(response.body.encrypted_data).toBe(testCard.encrypted_data);
});
it('DELETE /card/:card_id должен деактивировать карту', async () => {
const response = await request(app.getHttpServer())
.delete(`/card/${cardId}`)
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('is_active');
expect(response.body.id).toBe(cardId);
expect(response.body.is_active).toBe(false);
// Проверяем, что карта деактивирована в БД
const card = await cardRepository.findOne({
where: { id: cardId },
});
expect(card).toBeDefined();
if (card) {
expect(card.meta.is_active).toBe(false);
}
});
it('POST /card/issue с тем же username и coop_name должен обновить существующую карту', async () => {
// Создаем обновленные данные для карты
const updatedCard = {
...testCard,
encrypted_key: 'new-encrypted-key',
encrypted_data: 'new-encrypted-data',
meta: {
...testCard.meta,
card_type: 'premium',
},
};
const response = await request(app.getHttpServer())
.post('/card/issue')
.set('Authorization', `Bearer ${accessToken}`)
.send(updatedCard)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.id).toBe(cardId); // ID должен остаться тем же
expect(response.body.card_type).toBe('premium'); // Тип карты должен обновиться
// Проверяем, что данные карты обновились в БД
const card = await cardRepository.findOne({
where: { id: cardId },
});
expect(card).toBeDefined();
if (card) {
expect(card.encrypted_key).toBe('new-encrypted-key');
// Проверяем, что приватные данные также обновились
const privateData = await privateDataRepository.findOne({
where: { id: card.private_data_id },
});
expect(privateData).toBeDefined();
if (privateData) {
expect(privateData.encrypted_data).toBe('new-encrypted-data');
}
}
});
});
/**
* Тесты на проверку безопасности и прав доступа
*/
describe('Проверки безопасности', () => {
it('GET /card/user без авторизации должен вернуть ошибку 401', async () => {
await request(app.getHttpServer()).get('/card/user').expect(401);
});
it('GET /card/:card_id/private-data с неправильным ID карты должен вернуть ошибку', async () => {
await request(app.getHttpServer())
.get(`/card/invalid-id/private-data`)
.set('Authorization', `Bearer ${accessToken}`)
.expect(500); // или 404, в зависимости от реализации
});
});
});
+9 -2
View File
@@ -4,6 +4,13 @@
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
"^.+\\.(t|j)s$": ["ts-jest", {
"tsconfig": "<rootDir>/tsconfig.e2e.json"
}]
},
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/../src/$1"
},
"moduleDirectories": ["node_modules", "<rootDir>/../"],
"setupFilesAfterEnv": ["<rootDir>/jest.setup.js"]
}
+8
View File
@@ -0,0 +1,8 @@
// Настройка для корректного разрешения путей в тестах
const { pathsToModuleNameMapper } = require('ts-jest');
const { compilerOptions } = require('../tsconfig.json');
module.exports = {
// Другие настройки Jest
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths || {}, { prefix: '<rootDir>/' }),
};
+36
View File
@@ -0,0 +1,36 @@
// Скрипт для запуска e2e тестов с поддержкой путей
require('ts-node/register');
require('tsconfig-paths/register');
// Запускаем Jest программно
const { runCLI } = require('jest');
const config = {
config: JSON.stringify({
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: '.',
testEnvironment: 'node',
testRegex: '.e2e-spec.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
moduleNameMapper: {
'^src/(.*)$': '<rootDir>/../src/$1',
},
moduleDirectories: ['node_modules', '<rootDir>/../'],
}),
_: ['./test'],
runInBand: true,
detectOpenHandles: true,
};
runCLI(config, [process.cwd()])
.then((result) => {
if (!result.results.success) {
process.exit(1);
}
})
.catch((e) => {
console.error(e);
process.exit(1);
});
+39
View File
@@ -0,0 +1,39 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { AppModule } from '../src/app.module';
/**
* Создает и инициализирует тестовое приложение
* с настройками, аналогичными рабочему приложению
*/
export async function createTestingApp(): Promise<{
app: INestApplication;
moduleFixture: TestingModule;
}> {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();
const app = moduleFixture.createNestApplication();
// Применяем глобальные pipes, как в основном приложении
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}),
);
await app.init();
return { app, moduleFixture };
}
/**
* Очищает mock-и Jest после тестов
*/
export function clearJestMocks(): void {
jest.resetAllMocks();
jest.clearAllMocks();
}
+36
View File
@@ -0,0 +1,36 @@
/**
* Этот файл содержит функции для настройки тестового окружения
*/
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { AppModule } from '../src/app.module';
import { TypeOrmModule } from '@nestjs/typeorm';
/**
* Создает тестовое приложение для e2e тестов
* @returns Настроенное NestJS приложение
*/
export async function setupTestApp(): Promise<INestApplication> {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
const app = moduleFixture.createNestApplication();
await app.init();
return app;
}
/**
* Генерирует моковые значения для тестов
*/
export function generateTestData() {
return {
email: `test-${Math.random().toString(36).substring(7)}@example.com`,
password: 'password123',
hashKey: 'hashed_password_example', // В реальном приложении это хешируется клиентом
salt: 'test-salt-' + Math.random().toString(36).substring(7),
verificationCode: '123456',
};
}
@@ -0,0 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"paths": {
"src/*": ["../src/*"]
}
}
}
+3
View File
@@ -18,5 +18,8 @@
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"paths": {
"src/*": ["src/*"]
}
}
}

Some files were not shown because too many files have changed in this diff Show More