From fe879584f29e43b77f54dd17e621dc11c3928698 Mon Sep 17 00:00:00 2001 From: OxSaitama Date: Tue, 2 May 2023 15:12:12 +0200 Subject: [PATCH 1/2] add IPFS service --- package.json | 1 + src/app/api/super-admin/FilesController.ts | 150 ++++++++++++++++++ src/app/index.ts | 4 +- src/common/config/variables/Variables.ts | 16 ++ .../20230505075245_v1/migration.sql | 8 + src/common/databases/schema.prisma | 1 + src/common/databases/seeders/seeder.ts | 2 + src/common/helpers/ObjectHydrate.ts | 10 ++ src/common/repositories/FilesRepository.ts | 1 + .../CryptoService/CryptoService.ts | 74 +++++++++ .../FilesService/FilesService.ts | 21 ++- .../IpfsService/IpfsService.ts | 30 ++++ 12 files changed, 314 insertions(+), 4 deletions(-) create mode 100644 src/app/api/super-admin/FilesController.ts create mode 100644 src/common/databases/migrations/20230505075245_v1/migration.sql create mode 100644 src/services/private-services/CryptoService/CryptoService.ts create mode 100644 src/services/private-services/IpfsService/IpfsService.ts diff --git a/package.json b/package.json index 8a3c77e1..22753401 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ }, "homepage": "https://github.com/smart-chain-fr/leCoffre-back#readme", "dependencies": { + "@pinata/sdk": "^2.1.0", "@prisma/client": "^4.11.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", diff --git a/src/app/api/super-admin/FilesController.ts b/src/app/api/super-admin/FilesController.ts new file mode 100644 index 00000000..1fdd1696 --- /dev/null +++ b/src/app/api/super-admin/FilesController.ts @@ -0,0 +1,150 @@ +import { Response, Request } from "express"; +import { Controller, Delete, Get, Post, Put } from "@ControllerPattern/index"; +import ApiController from "@Common/system/controller-pattern/ApiController"; +import { Service } from "typedi"; +import FilesService from "@Services/private-services/FilesService/FilesService"; +import { Files } from "@prisma/client"; +import ObjectHydrate from "@Common/helpers/ObjectHydrate"; +import { File } from "le-coffre-resources/dist/SuperAdmin"; +import { validateOrReject } from "class-validator"; + +@Controller() +@Service() +export default class FilesController extends ApiController { + constructor(private filesService: FilesService) { + super(); + } + + /** + * @description Get all Files + * @returns File[] list of Files + */ + @Get("/api/v1/super-admin/files") + protected async get(req: Request, response: Response) { + try { + //get query + const query = JSON.parse(req.query["q"] as string); + + //call service to get prisma entity + const prismaEntity = await this.filesService.get(query); + + //Hydrate ressource with prisma entity + const files = File.map(File, prismaEntity, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, files); + } catch (error) { + this.httpBadRequest(response, error); + return; + } + } + + /** + * @description Create a new File + * @returns File created + */ + @Post("/api/v1/super-admin/files") + protected async post(req: Request, response: Response) { + try { + //init File resource with request body values + const fileEntity = File.hydrate(req.body); + + //validate File + await validateOrReject(fileEntity, { groups: ["createFile"] }); + + //call service to get prisma entity + const prismaEntityCreated = await this.filesService.create(fileEntity); + + //Hydrate ressource with prisma entity + const fileEntityCreated = File.hydrate(prismaEntityCreated, { + strategy: "excludeAll", + }); + + //success + this.httpSuccess(response, fileEntityCreated); + } catch (error) { + this.httpBadRequest(response, error); + return; + } + } + + /** + * @description Update a specific file + */ + @Put("/api/v1/super-admin/files/:uid") + protected async update(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + throw new Error("No uid provided"); + } + + //init File resource with request body values + const fileEntity = File.hydrate(req.body); + + //validate file + await validateOrReject(fileEntity, { groups: ["create"] }); + + //call service to get prisma entity + const prismaEntityUpdated: Files = await this.filesService.update(uid, fileEntity); + + //Hydrate ressource with prisma entity + const file = File.hydrate(prismaEntityUpdated, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, file); + } catch (error) { + this.httpBadRequest(response, error); + return; + } + } + + /** + * @description Delete a specific File + */ + @Delete("/api/v1/super-admin/files/:uid") + protected async delete(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + throw new Error("No uid provided"); + } + + //call service to get prisma entity + const fileEntity: Files = await this.filesService.delete(uid); + + //Hydrate ressource with prisma entity + const file = ObjectHydrate.hydrate(new File(), fileEntity, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, file); + } catch (error) { + this.httpBadRequest(response, error); + return; + } + } + + /** + * @description Get a specific File by uid + */ + @Get("/api/v1/super-admin/Files/:uid") + protected async getOneByUid(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + throw new Error("No uid provided"); + } + + const fileEntity = await this.filesService.getByUid(uid); + + //Hydrate ressource with prisma entity + const file = ObjectHydrate.hydrate(new File(), fileEntity, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, file); + } catch (error) { + this.httpBadRequest(response, error); + return; + } + } +} diff --git a/src/app/index.ts b/src/app/index.ts index 01e173ed..e2b0a51f 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -9,8 +9,8 @@ import DeedTypesController from "./api/super-admin/DeedTypesController"; import DocumentsController from "./api/super-admin/DocumentsController"; import DocumentTypesController from "./api/super-admin/DocumentTypesController"; import IdNotUserInfoController from "./api/idnot-user/UserInfoController"; - import DocumentsControllerCustomer from "./api/customer/DocumentsController"; +import FilesController from "./api/super-admin/FilesController"; /** @@ -28,7 +28,7 @@ export default { Container.get(DocumentsController); Container.get(DocumentTypesController); Container.get(IdNotUserInfoController); - + Container.get(FilesController); Container.get(DocumentsControllerCustomer); }, }; diff --git a/src/common/config/variables/Variables.ts b/src/common/config/variables/Variables.ts index 8624a386..43646886 100644 --- a/src/common/config/variables/Variables.ts +++ b/src/common/config/variables/Variables.ts @@ -45,6 +45,18 @@ export class BackendVariables { @IsNotEmpty() public readonly IDNOT_REDIRECT_URL!: string; + @IsNotEmpty() + public readonly PINATA_API_KEY!: string; + + @IsNotEmpty() + public readonly PINATA_API_SECRET!: string; + + @IsNotEmpty() + public readonly PINATA_GATEWAY!: string; + + @IsNotEmpty() + public readonly KEY_DATA!: string; + public constructor() { dotenv.config(); this.DATABASE_PORT = process.env["DATABASE_PORT"]!; @@ -60,6 +72,10 @@ export class BackendVariables { this.IDNOT_CLIENT_ID = process.env["IDNOT_CLIENT_ID"]!; this.IDNOT_CLIENT_SECRET = process.env["IDNOT_CLIENT_SECRET"]!; this.IDNOT_REDIRECT_URL = process.env["IDNOT_REDIRECT_URL"]!; + this.PINATA_API_KEY = process.env["PINATA_API_KEY"]!; + this.PINATA_API_SECRET = process.env["PINATA_API_SECRET"]!; + this.PINATA_GATEWAY = process.env["PINATA_GATEWAY"]!; + this.KEY_DATA = process.env["KEY_DATA"]!; } public async validate() { await validateOrReject(this); diff --git a/src/common/databases/migrations/20230505075245_v1/migration.sql b/src/common/databases/migrations/20230505075245_v1/migration.sql new file mode 100644 index 00000000..c212eab0 --- /dev/null +++ b/src/common/databases/migrations/20230505075245_v1/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `iv` to the `files` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "files" ADD COLUMN "iv" VARCHAR(255) NOT NULL; diff --git a/src/common/databases/schema.prisma b/src/common/databases/schema.prisma index 08317b33..9f156af6 100644 --- a/src/common/databases/schema.prisma +++ b/src/common/databases/schema.prisma @@ -203,6 +203,7 @@ model Files { document Documents @relation(fields: [document_uid], references: [uid], onDelete: Cascade) document_uid String @db.VarChar(255) file_path String @unique @db.VarChar(255) + iv String @db.VarChar(255) created_at DateTime? @default(now()) updated_at DateTime? @updatedAt diff --git a/src/common/databases/seeders/seeder.ts b/src/common/databases/seeders/seeder.ts index 35df0bbd..ab450e9f 100644 --- a/src/common/databases/seeders/seeder.ts +++ b/src/common/databases/seeders/seeder.ts @@ -380,6 +380,7 @@ import { uid: uidFiles1, document_uid: uidDocument1, file_path: "https://www.google1.com", + iv: "randomIv1", created_at: new Date(), updated_at: new Date(), }, @@ -387,6 +388,7 @@ import { uid: uidFiles2, document_uid: uidDocument2, file_path: "https://www.google2.com", + iv: "randomIv2", created_at: new Date(), updated_at: new Date(), }, diff --git a/src/common/helpers/ObjectHydrate.ts b/src/common/helpers/ObjectHydrate.ts index 199b8733..92cb3194 100644 --- a/src/common/helpers/ObjectHydrate.ts +++ b/src/common/helpers/ObjectHydrate.ts @@ -10,4 +10,14 @@ export default abstract class ObjectHydrate { return plainToInstance(ClassEntity, from, options); }); } + + // public static fromTypeToRessource(ClassEntity: { new (): T }, from: Record): T { + // const properties = Object.getOwnPropertyNames(ClassEntity); + // const classInstance = new ClassEntity() as T; + // properties.forEach((property) => { + // if (property in from) { + // classInstance[property] = from[property] as T[keyof T]; + // } + // }); + // } } diff --git a/src/common/repositories/FilesRepository.ts b/src/common/repositories/FilesRepository.ts index 7c7d99d8..54bf3bef 100644 --- a/src/common/repositories/FilesRepository.ts +++ b/src/common/repositories/FilesRepository.ts @@ -36,6 +36,7 @@ export default class FilesRepository extends BaseRepository { }, }, file_path: file.file_path, + iv: file.iv }, }); } diff --git a/src/services/private-services/CryptoService/CryptoService.ts b/src/services/private-services/CryptoService/CryptoService.ts new file mode 100644 index 00000000..c0ac9d5e --- /dev/null +++ b/src/services/private-services/CryptoService/CryptoService.ts @@ -0,0 +1,74 @@ +import BaseService from "@Services/BaseService"; +import { Service } from "typedi"; +import { BackendVariables } from "@Common/config/variables/Variables"; +import crypto from "crypto"; + +@Service() +export default class CryptoService extends BaseService { + private key: CryptoKey; + private jwkKey: any; + constructor(protected variables: BackendVariables) { + super(); + this.key = new CryptoKey(); + this.jwkKey = { + kty: "oct", + k: variables.KEY_DATA, + alg: "A256GCM", + ext: true, + }; + } + + private async getKey() { + if (!this.key) this.key = await crypto.subtle.importKey("jwk", this.jwkKey, {name: "AES-GCM"}, false, ["encrypt", "decrypt"]); + return this.key; + } + + public getTextEncoderDecoder() { + return { encoder: new TextEncoder(), decoder: new TextDecoder("utf-8") } + } + + /** + * @description : encrypt data + * @throws {Error} If data cannot be encrypted + */ + public async encrypt(data: any) { + const { encoder, decoder } = this.getTextEncoderDecoder(); + const encodedData = encoder.encode(data); + const iv = crypto.getRandomValues(new Uint8Array(16)); + const key = await this.getKey(); + const cipherData = await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv, + }, + key, + encodedData, + ); + + const cipherText = decoder.decode(cipherData); + const ivStringified = decoder.decode(iv); + + return { cipherText, ivStringified }; + } + + /** + * @description : decrypt data with an initialization vector + * @throws {Error} If data cannot be decrypted + */ + public async decrypt(cipherText: string, ivStringified: string): Promise { + const { encoder, decoder } = this.getTextEncoderDecoder(); + const cipherData = encoder.encode(cipherText); + const iv = encoder.encode(ivStringified); + const key = await this.getKey(); + const decryptedData = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv, + }, + key, + cipherData, + ); + + return decoder.decode(decryptedData); + } +} diff --git a/src/services/private-services/FilesService/FilesService.ts b/src/services/private-services/FilesService/FilesService.ts index 67656f16..ea97b2fc 100644 --- a/src/services/private-services/FilesService/FilesService.ts +++ b/src/services/private-services/FilesService/FilesService.ts @@ -2,10 +2,14 @@ import FilesRepository from "@Repositories/FilesRepository"; import BaseService from "@Services/BaseService"; import { Service } from "typedi"; import { File } from "le-coffre-resources/dist/SuperAdmin" +import CryptoService from "../CryptoService/CryptoService"; +import IpfsService from "../IpfsService/IpfsService"; +import fs from "fs"; +import { BackendVariables } from "@Common/config/variables/Variables"; @Service() export default class FilesService extends BaseService { - constructor(private filesRepository: FilesRepository) { + constructor(private filesRepository: FilesRepository, private ipfsService: IpfsService, private variables: BackendVariables, private cryptoService: CryptoService) { super(); } @@ -22,6 +26,11 @@ export default class FilesService extends BaseService { * @throws {Error} If file cannot be created */ public async create(file: File) { + const stream = fs.createReadStream('./login.png'); + const upload = await this.ipfsService.pinFile(stream, 'login.png'); + const encryptedPath = await this.cryptoService.encrypt(this.variables.PINATA_GATEWAY.concat(upload.IpfsHash)); + file.file_path = encryptedPath.cipherText; + file.iv = encryptedPath.ivStringified; return this.filesRepository.create(file); } @@ -29,10 +38,18 @@ export default class FilesService extends BaseService { * @description : Modify a new file * @throws {Error} If file cannot be modified */ - public async put(uid: string, file: File) { + public async update(uid: string, file: File) { return this.filesRepository.update(uid, file); } + /** + * @description : Delete a file + * @throws {Error} If file cannot be deleted + */ + public async delete(uid: string) { + return this.filesRepository.delete(uid); + } + /** * @description : Get a file by uid * @throws {Error} If project cannot be created diff --git a/src/services/private-services/IpfsService/IpfsService.ts b/src/services/private-services/IpfsService/IpfsService.ts new file mode 100644 index 00000000..95a1c4d1 --- /dev/null +++ b/src/services/private-services/IpfsService/IpfsService.ts @@ -0,0 +1,30 @@ +import BaseService from "@Services/BaseService"; +import { Service } from "typedi"; +import pinataSDK from "@pinata/sdk"; +import { BackendVariables } from "@Common/config/variables/Variables"; +import fs from "fs"; + +@Service() +export default class FilesService extends BaseService { + private ipfsClient: pinataSDK; + constructor(protected variables: BackendVariables) { + super(); + this.ipfsClient = new pinataSDK({ pinataApiKey: variables.PINATA_API_KEY, pinataSecretApiKey: variables.PINATA_API_SECRET }) + } + + /** + * @description : pin a file + * @throws {Error} If file cannot be pinned + */ + public async pinFile(stream: fs.ReadStream, fileName: string) { + return this.ipfsClient.pinFileToIPFS(stream, {pinataMetadata : {name: fileName}}); + } + + /** + * @description : unpin a file + * @throws {Error} If file cannot be unpinned + */ + public async unpinFile(hashToUnpin: string) { + return this.ipfsClient.unpin(hashToUnpin); + } +} From d893fe69062a6e45f40d759ab445f32b0aaa9668 Mon Sep 17 00:00:00 2001 From: OxSaitama Date: Mon, 8 May 2023 22:26:35 +0200 Subject: [PATCH 2/2] add formdata controller and delete file service --- package.json | 4 ++- src/app/api/super-admin/FilesController.ts | 15 +++++---- src/app/middlewares/FileHandler.ts | 22 +++++++++++++ .../20230505131655_v2/migration.sql | 8 +++++ src/common/databases/schema.prisma | 1 + src/common/databases/seeders/seeder.ts | 2 ++ src/common/repositories/FilesRepository.ts | 2 ++ src/entries/App.ts | 3 +- .../CryptoService/CryptoService.ts | 32 +++++++------------ .../FilesService/FilesService.ts | 13 +++++--- .../IpfsService/IpfsService.ts | 4 +-- 11 files changed, 72 insertions(+), 34 deletions(-) create mode 100644 src/app/middlewares/FileHandler.ts create mode 100644 src/common/databases/migrations/20230505131655_v2/migration.sql diff --git a/package.json b/package.json index 22753401..a286b223 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,9 @@ "cors": "^2.8.5", "express": "^4.18.2", "jsonwebtoken": "^9.0.0", - "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.37", + "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.38", "module-alias": "^2.2.2", + "multer": "^1.4.5-lts.1", "next": "^13.1.5", "node-cache": "^5.1.2", "node-schedule": "^2.1.1", @@ -66,6 +67,7 @@ "@types/express": "^4.17.16", "@types/jest": "^29.5.0", "@types/jsonwebtoken": "^9.0.1", + "@types/multer": "^1.4.7", "@types/node": "^18.11.18", "@types/node-schedule": "^2.1.0", "@types/uuid": "^9.0.0", diff --git a/src/app/api/super-admin/FilesController.ts b/src/app/api/super-admin/FilesController.ts index 1fdd1696..ff0c393c 100644 --- a/src/app/api/super-admin/FilesController.ts +++ b/src/app/api/super-admin/FilesController.ts @@ -4,7 +4,6 @@ import ApiController from "@Common/system/controller-pattern/ApiController"; import { Service } from "typedi"; import FilesService from "@Services/private-services/FilesService/FilesService"; import { Files } from "@prisma/client"; -import ObjectHydrate from "@Common/helpers/ObjectHydrate"; import { File } from "le-coffre-resources/dist/SuperAdmin"; import { validateOrReject } from "class-validator"; @@ -46,14 +45,18 @@ export default class FilesController extends ApiController { @Post("/api/v1/super-admin/files") protected async post(req: Request, response: Response) { try { - //init File resource with request body values - const fileEntity = File.hydrate(req.body); + //get file + if(!req.file) throw new Error('No file provided') + + //init File resource with request body values + const fileEntity = File.hydrate(JSON.parse(req.body["q"])); + //validate File await validateOrReject(fileEntity, { groups: ["createFile"] }); //call service to get prisma entity - const prismaEntityCreated = await this.filesService.create(fileEntity); + const prismaEntityCreated = await this.filesService.create(fileEntity, req.file); //Hydrate ressource with prisma entity const fileEntityCreated = File.hydrate(prismaEntityCreated, { @@ -114,7 +117,7 @@ export default class FilesController extends ApiController { const fileEntity: Files = await this.filesService.delete(uid); //Hydrate ressource with prisma entity - const file = ObjectHydrate.hydrate(new File(), fileEntity, { strategy: "excludeAll" }); + const file = File.hydrate(fileEntity, { strategy: "excludeAll" }); //success this.httpSuccess(response, file); @@ -138,7 +141,7 @@ export default class FilesController extends ApiController { const fileEntity = await this.filesService.getByUid(uid); //Hydrate ressource with prisma entity - const file = ObjectHydrate.hydrate(new File(), fileEntity, { strategy: "excludeAll" }); + const file = File.hydrate(fileEntity, { strategy: "excludeAll" }); //success this.httpSuccess(response, file); diff --git a/src/app/middlewares/FileHandler.ts b/src/app/middlewares/FileHandler.ts new file mode 100644 index 00000000..3a6138ac --- /dev/null +++ b/src/app/middlewares/FileHandler.ts @@ -0,0 +1,22 @@ +import { NextFunction, Request, Response } from "express"; +import multer from "multer"; + +export default function fileHandler(req: Request, response: Response, next: NextFunction) { + const storage = multer.memoryStorage() + const upload = multer({storage:storage}).single('file'); + + // Here call the upload middleware of multer + upload(req, response, function (err) { + if (err instanceof multer.MulterError) { + // A Multer error occurred when uploading. + const err = new Error('Multer error'); + return next(err) + } else if (err) { + // An unknown error occurred when uploading. + const err = new Error('Server Error') + return next(err) + } + next() + }) +} + diff --git a/src/common/databases/migrations/20230505131655_v2/migration.sql b/src/common/databases/migrations/20230505131655_v2/migration.sql new file mode 100644 index 00000000..a7f5aaa6 --- /dev/null +++ b/src/common/databases/migrations/20230505131655_v2/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `file_name` to the `files` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "files" ADD COLUMN "file_name" VARCHAR(255) NOT NULL; diff --git a/src/common/databases/schema.prisma b/src/common/databases/schema.prisma index 9f156af6..b1b04496 100644 --- a/src/common/databases/schema.prisma +++ b/src/common/databases/schema.prisma @@ -203,6 +203,7 @@ model Files { document Documents @relation(fields: [document_uid], references: [uid], onDelete: Cascade) document_uid String @db.VarChar(255) file_path String @unique @db.VarChar(255) + file_name String @db.VarChar(255) iv String @db.VarChar(255) created_at DateTime? @default(now()) updated_at DateTime? @updatedAt diff --git a/src/common/databases/seeders/seeder.ts b/src/common/databases/seeders/seeder.ts index ab450e9f..25dba6f3 100644 --- a/src/common/databases/seeders/seeder.ts +++ b/src/common/databases/seeders/seeder.ts @@ -379,6 +379,7 @@ import { { uid: uidFiles1, document_uid: uidDocument1, + file_name: "fileName1", file_path: "https://www.google1.com", iv: "randomIv1", created_at: new Date(), @@ -387,6 +388,7 @@ import { { uid: uidFiles2, document_uid: uidDocument2, + file_name: "fileName2", file_path: "https://www.google2.com", iv: "randomIv2", created_at: new Date(), diff --git a/src/common/repositories/FilesRepository.ts b/src/common/repositories/FilesRepository.ts index 54bf3bef..af5b8044 100644 --- a/src/common/repositories/FilesRepository.ts +++ b/src/common/repositories/FilesRepository.ts @@ -35,9 +35,11 @@ export default class FilesRepository extends BaseRepository { uid: file.document!.uid, }, }, + file_name: file.file_name, file_path: file.file_path, iv: file.iv }, + include: { document: true } }); } diff --git a/src/entries/App.ts b/src/entries/App.ts index b520f7a0..da76f6ae 100644 --- a/src/entries/App.ts +++ b/src/entries/App.ts @@ -8,6 +8,7 @@ import bodyParser from "body-parser"; // import TezosLink from "@Common/databases/TezosLink"; import errorHandler from "@App/middlewares/ErrorHandler"; import { BackendVariables } from "@Common/config/variables/Variables"; +import fileHandler from "@App/middlewares/FileHandler"; (async () => { try { @@ -22,7 +23,7 @@ import { BackendVariables } from "@Common/config/variables/Variables"; label, port: parseInt(port), rootUrl, - middlwares: [cors({ origin: "*" }), bodyParser.urlencoded({ extended: true }), bodyParser.json()], + middlwares: [cors({ origin: "*" }), fileHandler, bodyParser.urlencoded({ extended: true }), bodyParser.json()], errorHandler, }); diff --git a/src/services/private-services/CryptoService/CryptoService.ts b/src/services/private-services/CryptoService/CryptoService.ts index c0ac9d5e..23c7624a 100644 --- a/src/services/private-services/CryptoService/CryptoService.ts +++ b/src/services/private-services/CryptoService/CryptoService.ts @@ -5,11 +5,10 @@ import crypto from "crypto"; @Service() export default class CryptoService extends BaseService { - private key: CryptoKey; - private jwkKey: any; + private jwkKey: JsonWebKey; + private subtle: SubtleCrypto = crypto.webcrypto.subtle constructor(protected variables: BackendVariables) { super(); - this.key = new CryptoKey(); this.jwkKey = { kty: "oct", k: variables.KEY_DATA, @@ -19,24 +18,18 @@ export default class CryptoService extends BaseService { } private async getKey() { - if (!this.key) this.key = await crypto.subtle.importKey("jwk", this.jwkKey, {name: "AES-GCM"}, false, ["encrypt", "decrypt"]); - return this.key; - } - - public getTextEncoderDecoder() { - return { encoder: new TextEncoder(), decoder: new TextDecoder("utf-8") } + return await this.subtle.importKey("jwk", this.jwkKey, {name: "AES-GCM"}, false, ["encrypt", "decrypt"]); } /** * @description : encrypt data * @throws {Error} If data cannot be encrypted */ - public async encrypt(data: any) { - const { encoder, decoder } = this.getTextEncoderDecoder(); - const encodedData = encoder.encode(data); + public async encrypt(data: string) { + const encodedData = Buffer.from(data); const iv = crypto.getRandomValues(new Uint8Array(16)); const key = await this.getKey(); - const cipherData = await crypto.subtle.encrypt( + const cipherData = await this.subtle.encrypt( { name: "AES-GCM", iv, @@ -45,8 +38,8 @@ export default class CryptoService extends BaseService { encodedData, ); - const cipherText = decoder.decode(cipherData); - const ivStringified = decoder.decode(iv); + const cipherText = Buffer.from(cipherData).toString('base64'); + const ivStringified = Buffer.from(iv).toString('base64'); return { cipherText, ivStringified }; } @@ -56,11 +49,10 @@ export default class CryptoService extends BaseService { * @throws {Error} If data cannot be decrypted */ public async decrypt(cipherText: string, ivStringified: string): Promise { - const { encoder, decoder } = this.getTextEncoderDecoder(); - const cipherData = encoder.encode(cipherText); - const iv = encoder.encode(ivStringified); + const cipherData = Buffer.from(cipherText, 'base64'); + const iv = Buffer.from(ivStringified, 'base64'); const key = await this.getKey(); - const decryptedData = await crypto.subtle.decrypt( + const decryptedData = await this.subtle.decrypt( { name: "AES-GCM", iv, @@ -69,6 +61,6 @@ export default class CryptoService extends BaseService { cipherData, ); - return decoder.decode(decryptedData); + return Buffer.from(decryptedData).toString('utf-8'); } } diff --git a/src/services/private-services/FilesService/FilesService.ts b/src/services/private-services/FilesService/FilesService.ts index ea97b2fc..ce6373df 100644 --- a/src/services/private-services/FilesService/FilesService.ts +++ b/src/services/private-services/FilesService/FilesService.ts @@ -4,8 +4,9 @@ import { Service } from "typedi"; import { File } from "le-coffre-resources/dist/SuperAdmin" import CryptoService from "../CryptoService/CryptoService"; import IpfsService from "../IpfsService/IpfsService"; -import fs from "fs"; +//import fs from "fs"; import { BackendVariables } from "@Common/config/variables/Variables"; +import { Readable } from "stream"; @Service() export default class FilesService extends BaseService { @@ -25,10 +26,10 @@ export default class FilesService extends BaseService { * @description : Create a new file * @throws {Error} If file cannot be created */ - public async create(file: File) { - const stream = fs.createReadStream('./login.png'); - const upload = await this.ipfsService.pinFile(stream, 'login.png'); + public async create(file: File, fileData: Express.Multer.File) { + const upload = await this.ipfsService.pinFile(Readable.from(fileData.buffer), fileData.originalname); const encryptedPath = await this.cryptoService.encrypt(this.variables.PINATA_GATEWAY.concat(upload.IpfsHash)); + file.file_name = fileData.originalname; file.file_path = encryptedPath.cipherText; file.iv = encryptedPath.ivStringified; return this.filesRepository.create(file); @@ -47,6 +48,10 @@ export default class FilesService extends BaseService { * @throws {Error} If file cannot be deleted */ public async delete(uid: string) { + const fileToUnpin = await this.filesRepository.findOneByUid(uid); + const decryptedFilePath = await this.cryptoService.decrypt(fileToUnpin.file_path, fileToUnpin.iv); + const fileHash= decryptedFilePath.substring(this.variables.PINATA_GATEWAY.length); + await this.ipfsService.unpinFile(fileHash) return this.filesRepository.delete(uid); } diff --git a/src/services/private-services/IpfsService/IpfsService.ts b/src/services/private-services/IpfsService/IpfsService.ts index 95a1c4d1..cc0f3df7 100644 --- a/src/services/private-services/IpfsService/IpfsService.ts +++ b/src/services/private-services/IpfsService/IpfsService.ts @@ -2,7 +2,7 @@ import BaseService from "@Services/BaseService"; import { Service } from "typedi"; import pinataSDK from "@pinata/sdk"; import { BackendVariables } from "@Common/config/variables/Variables"; -import fs from "fs"; +import { Readable } from "stream"; @Service() export default class FilesService extends BaseService { @@ -16,7 +16,7 @@ export default class FilesService extends BaseService { * @description : pin a file * @throws {Error} If file cannot be pinned */ - public async pinFile(stream: fs.ReadStream, fileName: string) { + public async pinFile(stream: Readable, fileName: string) { return this.ipfsClient.pinFileToIPFS(stream, {pinataMetadata : {name: fileName}}); }