diff --git a/package.json b/package.json index 1cb06594..b9393cad 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@prisma/client": "^4.11.0", "@sentry/node": "^7.91.0", "adm-zip": "^0.5.10", + "aws-sdk": "^2.1556.0", "axios": "^1.6.2", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", @@ -58,7 +59,7 @@ "file-type-checker": "^1.0.8", "fp-ts": "^2.16.1", "jsonwebtoken": "^9.0.0", - "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.108", + "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.115", "module-alias": "^2.2.2", "monocle-ts": "^2.3.13", "multer": "^1.4.5-lts.1", diff --git a/src/app/api/customer/OfficeRibController.ts b/src/app/api/customer/OfficeRibController.ts new file mode 100644 index 00000000..f11538c8 --- /dev/null +++ b/src/app/api/customer/OfficeRibController.ts @@ -0,0 +1,43 @@ +import { Response, Request } from "express"; +import { Controller, Get } from "@ControllerPattern/index"; +import ApiController from "@Common/system/controller-pattern/ApiController"; + +import { Service } from "typedi"; +import OfficerRibService from "@Services/common/OfficeRibService/OfficeRibService"; +import authHandler from "@App/middlewares/AuthHandler"; +import OfficesService from "@Services/customer/OfficesService/OfficesService"; + +@Controller() +@Service() +export default class OfficeRibController extends ApiController { + constructor(private officeRibService: OfficerRibService, private officesService: OfficesService) { + super(); + } + + @Get("/api/v1/customer/office/:uid/rib", [authHandler]) + protected async getRibStream(req: Request, response: Response) { + const officeId = req.params["uid"]; + if (!officeId) throw new Error("No officeId provided"); + + const office = await this.officesService.getByUid(officeId, { address: true }); + + if (!office) { + this.httpNotFoundRequest(response, "Office not found"); + return; + } + + const fileName = office.rib_name; + if (!fileName) { + this.httpNotFoundRequest(response, "No file found"); + return; + } + + try { + const file = await this.officeRibService.getByUid(officeId, fileName!); + response.attachment(fileName!); + response.send(file.Body); + } catch (error) { + this.httpInternalError(response, error); + } + } +} diff --git a/src/app/api/notary/OfficeRibController.ts b/src/app/api/notary/OfficeRibController.ts new file mode 100644 index 00000000..d7330ad3 --- /dev/null +++ b/src/app/api/notary/OfficeRibController.ts @@ -0,0 +1,122 @@ +import { Response, Request } from "express"; +import { Controller, Delete, Get, Post } from "@ControllerPattern/index"; +import ApiController from "@Common/system/controller-pattern/ApiController"; + +import { Service } from "typedi"; +import OfficerRibService from "@Services/common/OfficeRibService/OfficeRibService"; +import authHandler from "@App/middlewares/AuthHandler"; +import OfficesService from "@Services/notary/OfficesService/OfficesService"; +import { Office as OfficeResource } from "le-coffre-resources/dist/Notary"; + +@Controller() +@Service() +export default class OfficeRibController extends ApiController { + constructor(private officeRibService: OfficerRibService, private officesService: OfficesService) { + super(); + } + + @Get("/api/v1/notary/office/rib", [authHandler]) + protected async getRibStream(req: Request, response: Response) { + const officeId: string = req.body.user.office_Id; + if (!officeId) throw new Error("No officeId provided"); + + const office = await this.officesService.getByUid(officeId, { address: true }); + + if (!office) { + this.httpNotFoundRequest(response, "Office not found"); + return; + } + + const fileName = office.rib_name; + if (!fileName) { + this.httpNotFoundRequest(response, "No file found"); + return; + } + + try { + const file = await this.officeRibService.getByUid(officeId, fileName!); + response.attachment(fileName!); + response.send(file.Body); + } catch (error) { + this.httpInternalError(response, error); + } + } + + @Post("/api/v1/notary/office/rib", [authHandler]) + protected async post(req: Request, response: Response) { + try { + const officeId: string = req.body.user.office_Id; + if (!req.file) throw new Error("No file provided"); + if (!officeId) throw new Error("No officeId provided"); + + const office = await this.officesService.getByUid(officeId, { address: true }); + + if (!office) { + this.httpNotFoundRequest(response, "office not found"); + return; + } + const fileUrl = await this.officeRibService.createOrUpdate(officeId, req.file); + + if (!fileUrl || !req.file.originalname) throw new Error("Error while uploading file"); + + office.rib_url = fileUrl; + office.rib_name = req.file.originalname; + + const officeEntity = OfficeResource.hydrate(office, { + strategy: "excludeAll", + }); + + //call service to get prisma entity + const officeEntityUpdated = await this.officesService.update(officeId, officeEntity); + //Hydrate ressource with prisma entity + const officeUpdated = OfficeResource.hydrate(officeEntityUpdated, { + strategy: "excludeAll", + }); + + //success + this.httpCreated(response, officeUpdated); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + @Delete("/api/v1/notary/office/rib", [authHandler]) + protected async delete(req: Request, response: Response) { + try { + const officeId: string = req.body.user.office_Id; + if (!officeId) throw new Error("No officeId provided"); + + const office = await this.officesService.getByUid(officeId, { address: true }); + + if (!office) { + this.httpNotFoundRequest(response, "office not found"); + return; + } + + const fileName = office.rib_name; + + await this.officeRibService.delete(officeId, fileName!); + + office.rib_url = null; + office.rib_name = null; + + const officeEntity = OfficeResource.hydrate(office, { + strategy: "excludeAll", + }); + + //call service to get prisma entity + const officeEntityUpdated = await this.officesService.update(officeId, officeEntity); + //Hydrate ressource with prisma entity + const officeUpdated = OfficeResource.hydrate(officeEntityUpdated, { + strategy: "excludeAll", + }); + + //success + this.httpCreated(response, officeUpdated); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } +} diff --git a/src/app/api/notary/OfficesController.ts b/src/app/api/notary/OfficesController.ts index ec418381..d7dac131 100644 --- a/src/app/api/notary/OfficesController.ts +++ b/src/app/api/notary/OfficesController.ts @@ -7,6 +7,7 @@ import { Offices, Prisma } from "@prisma/client"; import { Office as OfficeResource } from "le-coffre-resources/dist/Notary"; import ruleHandler from "@App/middlewares/RulesHandler"; import authHandler from "@App/middlewares/AuthHandler"; +import uuidMatcher from "@App/uuidMatcher"; @Controller() @Service() @@ -49,8 +50,9 @@ export default class OfficesController extends ApiController { /** * @description Get a specific office by uid + * `/:id(${uuidMatcher})` */ - @Get("/api/v1/notary/offices/:uid", [authHandler, ruleHandler]) + @Get(`/api/v1/notary/offices/:uid(${uuidMatcher})`, [authHandler, ruleHandler]) protected async getOneByUid(req: Request, response: Response) { try { const uid = req.params["uid"]; @@ -69,6 +71,7 @@ export default class OfficesController extends ApiController { } const officeEntity = await this.officesService.getByUid(uid, query); + if (!officeEntity) { this.httpNotFoundRequest(response, "office not found"); @@ -77,6 +80,8 @@ export default class OfficesController extends ApiController { //Hydrate ressource with prisma entity const office = OfficeResource.hydrate(officeEntity, { strategy: "excludeAll" }); + + //success this.httpSuccess(response, office); } catch (error) { diff --git a/src/app/index.ts b/src/app/index.ts index 2de03a00..510d903d 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -46,6 +46,8 @@ import DocumentControllerId360 from "./api/id360/DocumentController"; import CustomerControllerId360 from "./api/id360/CustomerController"; import UserNotificationController from "./api/notary/UserNotificationController"; import AuthController from "./api/customer/AuthController"; +import NotaryOfficeRibController from "./api/notary/OfficeRibController"; +import CustomerOfficeRibController from "./api/customer/OfficeRibController"; /** * @description This allow to declare all controllers used in the application @@ -100,5 +102,7 @@ export default { Container.get(DocumentControllerId360); Container.get(CustomerControllerId360); Container.get(AuthController); + Container.get(NotaryOfficeRibController); + Container.get(CustomerOfficeRibController); }, }; diff --git a/src/app/uuidMatcher.ts b/src/app/uuidMatcher.ts new file mode 100644 index 00000000..558fc1f3 --- /dev/null +++ b/src/app/uuidMatcher.ts @@ -0,0 +1,2 @@ +const uuidMatcher = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"; +export default uuidMatcher; \ No newline at end of file diff --git a/src/common/config/variables/Variables.ts b/src/common/config/variables/Variables.ts index 49ad7c2f..10df52e7 100644 --- a/src/common/config/variables/Variables.ts +++ b/src/common/config/variables/Variables.ts @@ -130,6 +130,18 @@ export class BackendVariables { @IsNotEmpty() public readonly SMS_FACTOR_TOKEN!: string; + @IsNotEmpty() + public readonly SCW_ACCESS_KEY_ID!: string; + + @IsNotEmpty() + public readonly SCW_ACCESS_KEY_SECRET!: string; + + @IsNotEmpty() + public readonly SCW_BUCKET_ENDPOINT!: string; + + @IsNotEmpty() + public readonly SCW_BUCKET_NAME!: string; + public constructor() { dotenv.config(); this.DATABASE_PORT = process.env["DATABASE_PORT"]!; @@ -174,6 +186,10 @@ export class BackendVariables { this.OVH_CONSUMER_KEY = process.env["OVH_CONSUMER_KEY"]!; this.OVH_SMS_SERVICE_NAME = process.env["OVH_SMS_SERVICE_NAME"]!; this.SMS_FACTOR_TOKEN = process.env["SMS_FACTOR_TOKEN"]!; + this.SCW_ACCESS_KEY_ID = process.env["SCW_ACCESS_KEY_ID"]!; + this.SCW_ACCESS_KEY_SECRET = process.env["SCW_ACCESS_KEY_SECRET"]!; + this.SCW_BUCKET_ENDPOINT = process.env["SCW_BUCKET_ENDPOINT"]!; + this.SCW_BUCKET_NAME = process.env["SCW_BUCKET_NAME"]!; } public async validate(groups?: string[]) { const validationOptions = groups ? { groups } : undefined; diff --git a/src/common/databases/migrations/20240215123932_rib/migration.sql b/src/common/databases/migrations/20240215123932_rib/migration.sql new file mode 100644 index 00000000..119d529e --- /dev/null +++ b/src/common/databases/migrations/20240215123932_rib/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "offices" ADD COLUMN "rib_url" VARCHAR(255) DEFAULT ''; diff --git a/src/common/databases/migrations/20240215142816_rib_optional/migration.sql b/src/common/databases/migrations/20240215142816_rib_optional/migration.sql new file mode 100644 index 00000000..20107944 --- /dev/null +++ b/src/common/databases/migrations/20240215142816_rib_optional/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "offices" ALTER COLUMN "rib_url" DROP DEFAULT; diff --git a/src/common/databases/migrations/20240216104224_rib_name/migration.sql b/src/common/databases/migrations/20240216104224_rib_name/migration.sql new file mode 100644 index 00000000..f733246c --- /dev/null +++ b/src/common/databases/migrations/20240216104224_rib_name/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "offices" ADD COLUMN "rib_name" VARCHAR(255); diff --git a/src/common/databases/schema.prisma b/src/common/databases/schema.prisma index de4afe97..06b7518b 100644 --- a/src/common/databases/schema.prisma +++ b/src/common/databases/schema.prisma @@ -92,6 +92,8 @@ model Offices { created_at DateTime? @default(now()) updated_at DateTime? @updatedAt checked_at DateTime? + rib_url String? @db.VarChar(255) + rib_name String? @db.VarChar(255) deed_types DeedTypes[] users Users[] office_folders OfficeFolders[] diff --git a/src/common/databases/seeders/prod-seeder.ts b/src/common/databases/seeders/prod-seeder.ts index b2e816bf..b6151603 100644 --- a/src/common/databases/seeders/prod-seeder.ts +++ b/src/common/databases/seeders/prod-seeder.ts @@ -1264,7 +1264,7 @@ export default async function main() { const documentTypeCreated = await prisma.documentTypes.create({ data: { name: documentType.name, - public_description: documentType.public_description, + public_description: documentType.public_description || "", private_description: documentType.private_description, office: { connect: { diff --git a/src/common/databases/seeders/seeder.ts b/src/common/databases/seeders/seeder.ts index 2e7b3f30..ce7e4cdf 100644 --- a/src/common/databases/seeders/seeder.ts +++ b/src/common/databases/seeders/seeder.ts @@ -1975,7 +1975,7 @@ export default async function main() { const documentTypeCreated = await prisma.documentTypes.create({ data: { name: documentType.name, - public_description: documentType.public_description, + public_description: documentType.public_description || "", private_description: documentType.private_description, office: { connect: { diff --git a/src/common/repositories/DocumentTypesRepository.ts b/src/common/repositories/DocumentTypesRepository.ts index 221e541f..790412b9 100644 --- a/src/common/repositories/DocumentTypesRepository.ts +++ b/src/common/repositories/DocumentTypesRepository.ts @@ -30,7 +30,7 @@ export default class DocumentTypesRepository extends BaseRepository { const createArgs: Prisma.DocumentTypesCreateArgs = { data: { name: documentType.name, - public_description: documentType.public_description, + public_description: documentType.public_description || "", private_description: documentType.private_description, office: { connect: { @@ -52,7 +52,7 @@ export default class DocumentTypesRepository extends BaseRepository { }, data: { name: documentType.name, - public_description: documentType.public_description, + public_description: documentType.public_description || "", private_description: documentType.private_description, archived_at: documentType.archived_at, }, diff --git a/src/common/repositories/OfficesRepository.ts b/src/common/repositories/OfficesRepository.ts index fb763880..a937bac0 100644 --- a/src/common/repositories/OfficesRepository.ts +++ b/src/common/repositories/OfficesRepository.ts @@ -67,6 +67,8 @@ export default class OfficesRepository extends BaseRepository { }, }, office_status: EOfficeStatus[office.office_status as keyof typeof EOfficeStatus], + rib_url: office.rib_url, + rib_name: office.rib_name, }, }; return this.model.update(updateArgs); diff --git a/src/services/common/IdNotService/IdNotService.ts b/src/services/common/IdNotService/IdNotService.ts index b9157b6d..047868af 100644 --- a/src/services/common/IdNotService/IdNotService.ts +++ b/src/services/common/IdNotService/IdNotService.ts @@ -121,7 +121,6 @@ export default class IdNotService extends BaseService { code: code, grant_type: "authorization_code", }); - console.log(this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL + "?" + query.toString()); const token = await fetch(this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL + "?" + query, { method: "POST" }); if(token.status !== 200) console.error(await token.text()); diff --git a/src/services/common/OfficeRibService/OfficeRibService.ts b/src/services/common/OfficeRibService/OfficeRibService.ts new file mode 100644 index 00000000..5ada46a8 --- /dev/null +++ b/src/services/common/OfficeRibService/OfficeRibService.ts @@ -0,0 +1,74 @@ +import BaseService from "@Services/BaseService"; +import { Service } from "typedi"; +import * as AWS from "aws-sdk"; +import { BackendVariables } from "@Common/config/variables/Variables"; +import path from "path"; + +@Service() +export default class OfficerRibService extends BaseService { + private readonly s3: AWS.S3; + constructor(private variables: BackendVariables) { + super(); + + // Configure the AWS SDK for Scaleway + this.s3 = new AWS.S3({ + accessKeyId: this.variables.SCW_ACCESS_KEY_ID, + secretAccessKey: this.variables.SCW_ACCESS_KEY_SECRET, + endpoint: this.variables.SCW_BUCKET_ENDPOINT, // Use the appropriate Scaleway endpoint + s3ForcePathStyle: true, // Needed for Scaleway's S3-compatible API + signatureVersion: "v4", + }); + } + + public async getByUid(uid: string, fileName: string) { + const key = path.join(this.variables.ENV, uid, fileName); + + return new Promise(async (resolve, reject) => { + this.s3.getObject( + { + Bucket: this.variables.SCW_BUCKET_NAME, + Key: key, + }, + function (err, data) { + if (err) return reject(err); + resolve(data); + }, + ); + }); + } + + public async createOrUpdate(officeId: string, file: Express.Multer.File) { + const key = path.join(this.variables.ENV, officeId, file.originalname); + + const uploadParams = { + Bucket: this.variables.SCW_BUCKET_NAME, + Key: key, // Example: 'example.txt' + Body: file.buffer, // Example: fs.createReadStream('/path/to/file') + ACL: "public-read", // Optional: Set the ACL if needed + }; + + return new Promise((resolve, reject) => { + this.s3.putObject(uploadParams, function (err, data) { + if (err) return reject(err); + resolve(`https://lecoffre-bucket.s3.fr-par.scw.cloud/lecoffre-bucket/${key}`); + }); + }); + } + + public async delete(officeId: string, fileName: string) { + const key = path.join(this.variables.ENV, officeId, fileName); + + return new Promise(async (resolve, reject) => { + this.s3.getObject( + { + Bucket: this.variables.SCW_BUCKET_NAME, + Key: key, + }, + function (err, data) { + if (err) return reject(err); + resolve(data); + }, + ); + }); + } +} diff --git a/src/services/customer/OfficesService/OfficesService.ts b/src/services/customer/OfficesService/OfficesService.ts new file mode 100644 index 00000000..8c7091d2 --- /dev/null +++ b/src/services/customer/OfficesService/OfficesService.ts @@ -0,0 +1,28 @@ +import {Prisma } from "@prisma/client"; +import OfficesRepository from "@Repositories/OfficesRepository"; +import BaseService from "@Services/BaseService"; +import { Service } from "typedi"; + + +@Service() +export default class OfficesService extends BaseService { + constructor(private officeRepository: OfficesRepository) { + super(); + } + + /** + * @description : Get all offices + * @throws {Error} If offices cannot be get + */ + public async get(query: Prisma.OfficesFindManyArgs) { + return this.officeRepository.findMany(query); + } + + /** + * @description : Get a office by uid + * @throws {Error} If office cannot be get + */ + public async getByUid(uid: string, query?: Prisma.OfficesInclude) { + return this.officeRepository.findOneByUid(uid, query); + } +} diff --git a/src/services/notary/OfficesService/OfficesService.ts b/src/services/notary/OfficesService/OfficesService.ts index af25083b..af0bb8dd 100644 --- a/src/services/notary/OfficesService/OfficesService.ts +++ b/src/services/notary/OfficesService/OfficesService.ts @@ -1,7 +1,9 @@ -import { Prisma } from "@prisma/client"; +import { Offices, Prisma } from "@prisma/client"; import OfficesRepository from "@Repositories/OfficesRepository"; import BaseService from "@Services/BaseService"; import { Service } from "typedi"; +import { Office as OfficeRessource } from "le-coffre-resources/dist/Notary"; + @Service() export default class OfficesService extends BaseService { @@ -24,4 +26,12 @@ export default class OfficesService extends BaseService { public async getByUid(uid: string, query?: Prisma.OfficesInclude) { return this.officeRepository.findOneByUid(uid, query); } + + /** + * @description : Modify an office + * @throws {Error} If office cannot be modified + */ + public async update(uid: string, officeEntity: OfficeRessource): Promise { + return this.officeRepository.update(uid, officeEntity); + } } diff --git a/src/test/config/Init.ts b/src/test/config/Init.ts index 4541cb77..919f934d 100644 --- a/src/test/config/Init.ts +++ b/src/test/config/Init.ts @@ -36,7 +36,7 @@ export const initDocumentType = (documentType: DocumentType, office: Office): Pr return prisma.documentTypes.create({ data: { name: documentType.name, - public_description: documentType.public_description, + public_description: documentType.public_description || "" , private_description: documentType.private_description, archived_at: null, office_uid: office.uid!,