Обновление конфигурации и кода: добавлены новые переменные окружения для PostgreSQL, изменены зависимости в package.json, обновлены схемы и интерфейсы, улучшена обработка данных в сервисах и контроллерах. Удалены неиспользуемые файлы и исправлены ошибки в коде.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description:
|
||||
globs: components/backend/**
|
||||
alwaysApply: false
|
||||
---
|
||||
1. Мы пишем бэкенд на nestjs в чистой архитектуре трех слоёв: домена, инфраструктуры и приложения.
|
||||
2. Пиши комментарии ко всему что делаешь, чтобы разобраться было просто.
|
||||
3. Для того чтобы на фронтенде появились методы - необходимо создать клиент через pnpm generate-client.
|
||||
@@ -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'.
|
||||
@@ -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
@@ -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
@@ -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>;
|
||||
}
|
||||
|
||||
+48
-17
@@ -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;
|
||||
}
|
||||
|
||||
+60
-17
@@ -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":";;;;;;;;;;;;;;;AAAA,2CAAyE;AACzE,qEAGyC;AACzC,+BAAoC;AACpC,oCAAoC;AACpC,+EAG8C;AAC9C,iFAG+C;AAC/C,yDAA+C;AAC/C,+EAAyE;AAIlE,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAC5B,YACmB,MAAqB,EACI,cAA8B,EAEvD,kBAAsC,EAEtC,mBAAwC;QALxC,WAAM,GAAN,MAAM,CAAe;QACI,mBAAc,GAAd,cAAc,CAAgB;QAEvD,uBAAkB,GAAlB,kBAAkB,CAAoB;QAEtC,wBAAmB,GAAnB,mBAAmB,CAAqB;IACxD,CAAC;IAEJ,KAAK,CAAC,oBAAoB,CAAC,KAAa;QACtC,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAClE,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,IAAI,4BAAmB,CAAC,qBAAqB,CAAC,CAAC;QACvD,CAAC;QAED,MAAM,IAAI,GAAG,IAAA,SAAM,GAAE,CAAC;QACtB,MAAM,IAAI,GAAG,IAAA,SAAM,GAAE,CAAC;QACtB,MAAM,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CACrC,IAAI,EACJ,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,CACvD,CAAC;QAEF,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,oBAAoB,CACxB,KAAa,EACb,QAAgB,EAChB,IAAY,EACZ,IAAY;QAEZ,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAErE,IAAI,CAAC,WAAW;YAAE,MAAM,IAAI,4BAAmB,CAAC,yBAAyB,CAAC,CAAC;QAE3E,MAAM,IAAI,GAAG,IAAI,kBAAI,CAAC;YACpB,EAAE,EAAE,IAAA,SAAM,GAAE;YACZ,KAAK;YACL,IAAI;YACJ,QAAQ;SACT,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAEvC,MAAM,YAAY,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACvD,MAAM,aAAa,GAAG,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAGzD,MAAM,IAAI,CAAC,kBAAkB,CAAC,OAAO,CACnC,YAAY,EACZ,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,yBAAyB,CAAC,CACzD,CAAC;QACF,MAAM,IAAI,CAAC,kBAAkB,CAAC,OAAO,CACnC,aAAa,EACb,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,0BAA0B,CAAC,CAC1D,CAAC;QAEF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,KAAa;QAC/B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,4BAAmB,CAAC,gBAAgB,CAAC,CAAC;QAClD,CAAC;QAED,MAAM,IAAI,GAAG,IAAA,SAAM,GAAE,CAAC;QACtB,MAAM,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CACrC,IAAI,EACJ,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,qBAAqB,CAAC,CACzD,CAAC;QAEF,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,KAAa,EAAE,QAAgB,EAAE,IAAY;QAC/D,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACrE,IAAI,CAAC,WAAW;YAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAE7D,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACzC,CAAC;QAED,MAAM,YAAY,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACvD,MAAM,aAAa,GAAG,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEzD,MAAM,IAAI,CAAC,kBAAkB,CAAC,OAAO,CACnC,YAAY,EACZ,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,yBAAyB,CAAC,CACzD,CAAC;QACF,MAAM,IAAI,CAAC,kBAAkB,CAAC,OAAO,CACnC,aAAa,EACb,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,0BAA0B,CAAC,CAC1D,CAAC;QAEF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAgC;QAC3C,MAAM,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC3D,MAAM,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC9D,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,YAAoB;QAC3C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;QACvE,IAAI,CAAC,OAAO;YACV,MAAM,IAAI,4BAAmB,CAAC,kCAAkC,CAAC,CAAC;QAEpE,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,YAAY,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAQ,CAAC;YACvE,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC;YAE5B,MAAM,YAAY,GAAG,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;YACvD,MAAM,eAAe,GAAG,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;YAE3D,MAAM,IAAI,CAAC,kBAAkB,CAAC,OAAO,CACnC,YAAY,EACZ,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,yBAAyB,CAAC,CACzD,CAAC;YACF,MAAM,IAAI,CAAC,kBAAkB,CAAC,OAAO,CACnC,eAAe,EACf,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,0BAA0B,CAAC,CAC1D,CAAC;YAEF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,CAAC;QAC1D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,4BAAmB,CAAC,uBAAuB,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,QAAgB;QAC9B,MAAM,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,IAAY;QAC3B,MAAM,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC;IAEO,mBAAmB,CAAC,OAAe;QACzC,OAAO,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE;YACvD,SAAS,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,yBAAyB,GAAG;SACvD,CAAC,CAAC;IACL,CAAC;IAEO,oBAAoB,CAAC,OAAe;QAC1C,OAAO,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE;YACvD,SAAS,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,0BAA0B,GAAG;SACxD,CAAC,CAAC;IACL,CAAC;IAEO,YAAY,CAAC,IAAY;QAC/B,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC3D,CAAC;CACF,CAAA;AA3JY,8CAAiB;4BAAjB,iBAAiB;IAD7B,IAAA,mBAAU,GAAE;IAIR,WAAA,IAAA,eAAM,EAAC,iCAAe,CAAC,CAAA;IACvB,WAAA,IAAA,eAAM,EAAC,2CAAoB,CAAC,CAAA;IAE5B,WAAA,IAAA,eAAM,EAAC,6CAAqB,CAAC,CAAA;qCAJL,8BAAa;GAF7B,iBAAiB,CA2J7B"}
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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",
|
||||
|
||||
@@ -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,7 +60,9 @@ 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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export class InitiateLoginResponseDTO {
|
||||
})
|
||||
salt!: string;
|
||||
|
||||
constructor(data: InitiateLoginResponseDTO){
|
||||
Object.assign(this, data)
|
||||
constructor(data: InitiateLoginResponseDTO) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export class InitiateRegistrationResponseDTO {
|
||||
})
|
||||
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',
|
||||
@@ -33,6 +48,7 @@ export class IssueCardInputDTO {
|
||||
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);
|
||||
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);
|
||||
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(refreshToken: string): Promise<void> {
|
||||
// В простейшем случае можно просто не принимать больше этот refreshToken
|
||||
// Для этого необходимо хранить список отозванных токенов или использовать JWT с коротким сроком жизни
|
||||
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,15 +6,15 @@ 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,
|
||||
@@ -24,9 +24,10 @@ import { InfrastructureModule } from 'src/infrastructure/infrastructure.module';
|
||||
UserDomainService,
|
||||
CardDomainService,
|
||||
AccessDomainService,
|
||||
PrivateDataDomainService,
|
||||
AccessInteractor,
|
||||
CardInteractor,
|
||||
UserInteractor,
|
||||
]
|
||||
],
|
||||
})
|
||||
export class DomainModule {}
|
||||
|
||||
@@ -1,36 +1,18 @@
|
||||
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()
|
||||
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
|
||||
@@ -44,5 +26,4 @@ export class AccessRequest {
|
||||
}
|
||||
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}`,
|
||||
`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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,54 +1,81 @@
|
||||
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,
|
||||
public_key,
|
||||
announce: publicData.announce,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async shareData(userId: string, dto: ShareDataDTO): Promise<string> {
|
||||
const card = await this.cardRepository.findByUsername(dto.username)
|
||||
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 (!card) throw new BadRequestException('Карта не найдена');
|
||||
|
||||
if (userId != card.userId)
|
||||
if (user_id != card.user_id)
|
||||
throw new BadRequestException('Нельзя воспользоваться чужой картой');
|
||||
|
||||
const accessRequest = new AccessRequest({
|
||||
@@ -67,13 +94,15 @@ export class AccessInteractor {
|
||||
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,19 +111,26 @@ 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
|
||||
accessRequest.ticket_is_used = true;
|
||||
|
||||
await this.accessRepository.save(accessRequest)
|
||||
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,
|
||||
@@ -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 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;
|
||||
/**
|
||||
* Интерфейс для описания данных, необходимых для выпуска карты
|
||||
*/
|
||||
export interface IssueCardDomainInterface {
|
||||
user_id: string; // ID пользователя-владельца
|
||||
username: string; // Имя пользователя в кооперативе
|
||||
coop_name: string; // Название кооператива
|
||||
encrypted_key: string; // Зашифрованный ключ для доступа к кооперативу
|
||||
encrypted_data: string; // Зашифрованные приватные данные
|
||||
meta: {
|
||||
version: number;
|
||||
}
|
||||
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) {}
|
||||
constructor(
|
||||
@Inject(CARD_REPOSITORY) private readonly cardRepository: CardRepository,
|
||||
private readonly privateDataService: PrivateDataDomainService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Выпускает новую карту пайщика
|
||||
*
|
||||
* @param data Данные для выпуска карты
|
||||
* @returns Сохраненная карта
|
||||
*/
|
||||
async issueCard(data: IssueCardDomainInterface): Promise<Card> {
|
||||
// хэшируем данные карты для использования в процессе верификации
|
||||
// будем передавать в объект user контракта registrator как обязательный параметр при регистрации
|
||||
const data_hash = sha256(data.encrypted_data)
|
||||
// Сохраняем или обновляем приватные данные
|
||||
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: v4(),
|
||||
id: existingCard?.id || v4(),
|
||||
username: data.username,
|
||||
encrypted_data: data.encrypted_data,
|
||||
user_id: data.user_id,
|
||||
private_data_id: privateData.id,
|
||||
coop_name: data.coop_name,
|
||||
encrypted_key: data.encrypted_key,
|
||||
userId: data.userId,
|
||||
data_hash,
|
||||
meta: {...data.meta, issued_at: new Date()}
|
||||
meta: {
|
||||
...data.meta,
|
||||
issued_at: new Date(),
|
||||
card_type: data.meta?.card_type || 'standard',
|
||||
is_active: true,
|
||||
},
|
||||
});
|
||||
|
||||
// ищем карту пользователя с указанным 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
|
||||
|
||||
// сохраняем карту в репозитории
|
||||
// Сохраняем карту в репозитории
|
||||
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,31 +1,59 @@
|
||||
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,
|
||||
@@ -35,27 +63,42 @@ export class UserDomainService {
|
||||
|
||||
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');
|
||||
}
|
||||
@@ -63,29 +106,71 @@ export class UserDomainService {
|
||||
const access_token = this.generateAccessToken(user.id);
|
||||
const refresh_token = this.generateRefreshToken(user.id);
|
||||
|
||||
return {access_token, refresh_token};
|
||||
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 generateAccessToken(userId: string): string {
|
||||
return jwt.sign({ sub: userId }, process.env.JWT_SECRET as string, { expiresIn: '30d' });
|
||||
async logout(data: LogoutInputDomainInterface): Promise<void> {
|
||||
await this.jwtTokenRepository.revokeJwt(data.access_token);
|
||||
await this.jwtTokenRepository.revokeJwt(data.refresh_token);
|
||||
}
|
||||
|
||||
private generateRefreshToken(userId: string): string {
|
||||
return jwt.sign({ sub: userId }, process.env.JWT_SECRET as string, { expiresIn: '180d' });
|
||||
}
|
||||
async refreshAccessToken(refreshToken: string) {
|
||||
const isValid = await this.jwtTokenRepository.isJwtValid(refreshToken);
|
||||
if (!isValid)
|
||||
throw new BadRequestException('Invalid or revoked refresh token');
|
||||
|
||||
async refreshAccessToken(refreshToken: string): Promise<CompleteLoginResponseDomainInterface> {
|
||||
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;
|
||||
+18
-8
@@ -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 } });
|
||||
}
|
||||
async findByUsernameAndUserId(username: string, userId: string): Promise<Card | null> {
|
||||
if (!username || userId) {
|
||||
throw new BadRequestException('Username and userId must be provided');
|
||||
const result = await this.repository.findOne({ where: { id } });
|
||||
return result ? new Card(result) : null;
|
||||
}
|
||||
|
||||
return this.repository.findOne({ where: { username, userId } });
|
||||
/**
|
||||
* Ищет карту по имени пользователя и названию кооператива
|
||||
* @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');
|
||||
}
|
||||
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()) });
|
||||
}
|
||||
}
|
||||
+62
@@ -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({
|
||||
ConfigModule,
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
type: 'postgres',
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
database: 'cardcoop',
|
||||
host: configService.dbHost,
|
||||
port: configService.dbPort,
|
||||
username: configService.dbUsername,
|
||||
password: configService.dbPassword,
|
||||
database: configService.dbDatabase,
|
||||
synchronize: true,
|
||||
entities: [
|
||||
User,
|
||||
Card,
|
||||
AccessRequest,
|
||||
Coop,
|
||||
Token
|
||||
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
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
+249
-10
@@ -170,16 +170,17 @@
|
||||
"post": {
|
||||
"description": "Выход",
|
||||
"operationId": "logout",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$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';
|
||||
@@ -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, в зависимости от реализации
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Настройка для корректного разрешения путей в тестах
|
||||
const { pathsToModuleNameMapper } = require('ts-jest');
|
||||
const { compilerOptions } = require('../tsconfig.json');
|
||||
|
||||
module.exports = {
|
||||
// Другие настройки Jest
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths || {}, { prefix: '<rootDir>/' }),
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user