merged dev

This commit is contained in:
Vins 2023-09-22 11:36:35 +02:00
commit 51970e0435
34 changed files with 2007 additions and 1548 deletions

View File

@ -20,7 +20,7 @@ jobs:
- setup_remote_docker: - setup_remote_docker:
version: 20.10.12 version: 20.10.12
docker_layer_caching: true docker_layer_caching: true
- run: docker login rg.fr-par.scw.cloud/lecoffre -u nologin -p $SCW_SECRET_KEY - run: docker login rg.fr-par.scw.cloud/lecoffre -u nologin -p $SCW_SECRET_KEY_BIS
- run: docker build --tag rg.fr-par.scw.cloud/lecoffre/back:${CIRCLE_SHA1:0:7} . - run: docker build --tag rg.fr-par.scw.cloud/lecoffre/back:${CIRCLE_SHA1:0:7} .
- run: docker push rg.fr-par.scw.cloud/lecoffre/back:${CIRCLE_SHA1:0:7} - run: docker push rg.fr-par.scw.cloud/lecoffre/back:${CIRCLE_SHA1:0:7}
@ -33,7 +33,7 @@ jobs:
parameters: parameters:
env: env:
type: string type: string
default: stg default: ppd
steps: steps:
- checkout - checkout
- kubernetes/install-kubeconfig: - kubernetes/install-kubeconfig:

View File

@ -1,6 +1,6 @@
dockerPullSecret: docker-pull-secret dockerPullSecret: docker-pull-secret
scwSecretKey: AgCgjF5QEzxT3GYTS5B6cmQ0e+0/qFWzKaUDSi+Vjc7RoameuvaIJvTXMBkS3he1oy1ulbB34v6vpZI2kxnGNqERA/U5BaYDAyfKSBwMAy4br7HVKhhuwkoF5qoG5JzJXseSmqB1U9vncVIGOZWzJc1Y4/eGlWcvLcLyfw2z/WEpyeNiWJfEhTYpJOB7gv0XnRb2U/JM3jRy1QgEUIk1WR6kgBalF+xaczPQ6uKh+PR2pqkbZa3WaKUrddmzNsgEz4d8PZMWt8IBwR2JOQEHUqCd34p/pJNyLdUgcdDhg02DKwn1oRoAxKTbAio/a7WrMbodjCb3TNWIYGal5mFmItZ7Ok/EBmUf4E85eOkTR+j8ynuuiexld3Q5Kw3o8LsHjgzVL9uP+T2rYaKkjtVt+YQRX1U8l9CrsdUEz0/wEBA0jwCWMfnh1qhD5pM/xwwjsEEAcK4rYV+Q7iAgGZZvZBCQ5aEHzrtn5D95tr1GZCV2hmrW6Seu+LKKLVBS1JmsuEsOuhudYsEK9m2RYVcxbjuS5eokKEjNrGobf2oB8rhBByavfw1JTBixR5JrI8lcYlnCa+oEhxXKJY+4Fx5SAB4YaLCMSo5vw6zsFQ3WKQzlEmCFt+EnapS+a+MGrdlwq07OHTDpvgk/1z39hopoCuhhKckGGfErLXsTYQvDOkFu+EPzgY7m7qDw/d9pSiht5tuSOkAqeOgm7tpNkUufZhaXmP+1aT7i+H5gq1JILGAmXzTI5Wc= scwSecretKey: AgChoEnPitXp4Ny/rVMEcevaWKNVpyj2cJYAcq+yFqKwVwnLB+ffDvwqz9XBHu+6d4Nyyjkf37zUAMoaM21lEDWA7x3zfG2/D/j+rvX1qxzZgLD0mjBk7fGElVm332I6JA83oInes8AMMYEDPLElzHnpKRb9KtkIP4NzgOcCeW0ijft3N7Vroez6LEHsBPCA1I9XjKSkGEDvrO0MhWX3iJOlfz+SPMfJAV7rPawOs0ZmohTHrPW8qIvGDn8HCzKyU8zRBoMt+Ogpf5pH4U3JryEFuqD61KAQgablAM8edPIvsgNno9HAEuC2QtRLYA9aUhuKdaKuS58c9P2E80PHWXIlbpFCg6EugQTgNfnYp+3qDUNz8edeCfapYLvF4s9eCMGyMsGnpDR8EDNOyuGy7Y3l7okX8Xqu464gMp9E+hX7bHkcD6a4xfyIgJcWxsku0tm1TH1dpn4M1UXRuyZZif8P08nuE6MTUL67sAR9J1lpn4lVEL4kflk0pP2tZ5ncgPQFafJrRz05krMb0eU5tb2H4gs7ao/LL6idWo8MM9K1yr8lIuT5x2WW5CX+RjA+i50ex114V6vX3PNP5oVyt+DynTUB9QmXzVm3oLfDc3Cae1uqh7X0CFd+xiztJBtg0VtJaD/xUJcuWfY4cV2lERo9fRrykltzlJqiXHO4nowt8OtN0BcViVV8NJhPhYFzyb4ympxpOlTjm3GETuT2TYhUqdgS9nzleEAbOmOHZdIO2COunPE=
lecoffreBack: lecoffreBack:
serviceAccountName: lecoffre-back-sa serviceAccountName: lecoffre-back-sa
@ -18,10 +18,10 @@ lecoffreBack:
limits: limits:
memory: 2Gi memory: 2Gi
ingress: ingress:
host: api.stg.lecoffre.smart-chain.fr host: api.ppd.lecoffre.smart-chain.fr
tls: tls:
hosts: hosts:
- api.stg.lecoffre.smart-chain.fr - api.ppd.lecoffre.smart-chain.fr
secretName: api-tls secretName: api-tls
annotations: annotations:
kubernetes.io/ingress.class: nginx kubernetes.io/ingress.class: nginx
@ -30,11 +30,11 @@ lecoffreBack:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true" nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
env: env:
- key: .env - key: .env
scwID: "id:a131edea-84e0-49d6-b4a8-20ab417220c9" scwID: "id:2be9510b-bb1f-4fbe-ab3e-3dc11fb49051"
lecoffreCron: lecoffreCron:
serviceAccountName: lecoffre-cron-sa serviceAccountName: lecoffre-cron-sa
envSecrets: stg-env envSecrets: ppd-env
command: "'sh', '-c', 'export $(xargs </etc/env/.env) && npm run cron'" command: "'sh', '-c', 'export $(xargs </etc/env/.env) && npm run cron'"
imagePullSecrets: imagePullSecrets:
- name: docker-pull-secret - name: docker-pull-secret

View File

@ -34,7 +34,7 @@ lecoffreBack:
lecoffreCron: lecoffreCron:
serviceAccountName: lecoffre-cron-sa serviceAccountName: lecoffre-cron-sa
envSecrets: stg-env envSecrets: prd-env
command: "'sh', '-c', 'export $(xargs </etc/env/.env) && npm run cron'" command: "'sh', '-c', 'export $(xargs </etc/env/.env) && npm run cron'"
imagePullSecrets: imagePullSecrets:
- name: docker-pull-secret - name: docker-pull-secret

View File

@ -1,6 +1,6 @@
dockerPullSecret: id:47c1c301-1f02-49cc-9e5c-c1225a770aa8 dockerPullSecret: docker-pull-secret
scwSecretKey: AgChoEnPitXp4Ny/rVMEcevaWKNVpyj2cJYAcq+yFqKwVwnLB+ffDvwqz9XBHu+6d4Nyyjkf37zUAMoaM21lEDWA7x3zfG2/D/j+rvX1qxzZgLD0mjBk7fGElVm332I6JA83oInes8AMMYEDPLElzHnpKRb9KtkIP4NzgOcCeW0ijft3N7Vroez6LEHsBPCA1I9XjKSkGEDvrO0MhWX3iJOlfz+SPMfJAV7rPawOs0ZmohTHrPW8qIvGDn8HCzKyU8zRBoMt+Ogpf5pH4U3JryEFuqD61KAQgablAM8edPIvsgNno9HAEuC2QtRLYA9aUhuKdaKuS58c9P2E80PHWXIlbpFCg6EugQTgNfnYp+3qDUNz8edeCfapYLvF4s9eCMGyMsGnpDR8EDNOyuGy7Y3l7okX8Xqu464gMp9E+hX7bHkcD6a4xfyIgJcWxsku0tm1TH1dpn4M1UXRuyZZif8P08nuE6MTUL67sAR9J1lpn4lVEL4kflk0pP2tZ5ncgPQFafJrRz05krMb0eU5tb2H4gs7ao/LL6idWo8MM9K1yr8lIuT5x2WW5CX+RjA+i50ex114V6vX3PNP5oVyt+DynTUB9QmXzVm3oLfDc3Cae1uqh7X0CFd+xiztJBtg0VtJaD/xUJcuWfY4cV2lERo9fRrykltzlJqiXHO4nowt8OtN0BcViVV8NJhPhYFzyb4ympxpOlTjm3GETuT2TYhUqdgS9nzleEAbOmOHZdIO2COunPE= scwSecretKey: 59bcf27d-bee3-4d14-8b4d-03fd6a8be6cd
lecoffreBack: lecoffreBack:
serviceAccountName: lecoffre-back-sa serviceAccountName: lecoffre-back-sa
@ -51,6 +51,4 @@ lecoffreCron:
# key is name of the environment variable, scwID is the secret ID in SCW with "id:" in front # key is name of the environment variable, scwID is the secret ID in SCW with "id:" in front
env: env:
- key: .env - key: .env
scwID: "id:2be9510b-bb1f-4fbe-ab3e-3dc11fb49051" scwID: "id:2be9510b-bb1f-4fbe-ab3e-3dc11fb49051"

View File

@ -1,7 +1,7 @@
apiVersion: v1 apiVersion: v1
kind: ServiceAccount kind: ServiceAccount
metadata: metadata:
name: {{ .Values.lecoffreBack.serviceAccountName }} name: {{ .Values.lecoffreBack.serviceAccountName }}
--- ---
@ -10,14 +10,14 @@ kind: Secret
metadata: metadata:
name: {{ .Values.lecoffreBack.serviceAccountName }}-token name: {{ .Values.lecoffreBack.serviceAccountName }}-token
annotations: annotations:
kubernetes.io/service-account.name: {{ .Values.lecoffreBack.serviceAccountName }} kubernetes.io/service-account.name: {{ .Values.lecoffreBack.serviceAccountName }}
type: kubernetes.io/service-account-token type: kubernetes.io/service-account-token
--- ---
apiVersion: v1 apiVersion: v1
kind: ServiceAccount kind: ServiceAccount
metadata: metadata:
name: {{ .Values.lecoffreCron.serviceAccountName }} name: {{ .Values.lecoffreCron.serviceAccountName }}
--- ---

View File

@ -19,10 +19,10 @@ lecoffreBack:
limits: limits:
memory: 2Gi memory: 2Gi
ingress: ingress:
host: api.stg.lecoffre.smart-chain.fr host: api.ppd.lecoffre.smart-chain.fr
tls: tls:
hosts: hosts:
- api.stg.lecoffre.smart-chain.fr - api.ppd.lecoffre.smart-chain.fr
secretName: api-tls secretName: api-tls
annotations: annotations:
kubernetes.io/ingress.class: nginx kubernetes.io/ingress.class: nginx

1604
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -51,9 +51,11 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"cron": "^2.3.1", "cron": "^2.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"fp-ts": "^2.16.1",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.79", "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.79",
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"monocle-ts": "^2.3.13",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"next": "^13.1.5", "next": "^13.1.5",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",

View File

@ -6,6 +6,7 @@ import ApiController from "@Common/system/controller-pattern/ApiController";
@Controller() @Controller()
@Service() @Service()
export default class HomeController extends ApiController { export default class HomeController extends ApiController {
@Get("/") @Get("/")
protected async get(req: Request, res: Response) { protected async get(req: Request, res: Response) {
// const query = processFindManyQuery(req.query); // const query = processFindManyQuery(req.query);

View File

@ -92,9 +92,7 @@ export default class DocumentsController extends ApiController {
protected async post(req: Request, response: Response) { protected async post(req: Request, response: Response) {
try { try {
//init Document resource with request body values //init Document resource with request body values
const documentEntity = Document.hydrate<Document>(req.body); const documentEntity = Document.hydrate<Document>(req.body);
console.log(documentEntity);
//validate document //validate document
await validateOrReject(documentEntity, { groups: ["createDocument"], forbidUnknownValues: false }); await validateOrReject(documentEntity, { groups: ["createDocument"], forbidUnknownValues: false });

View File

@ -54,7 +54,7 @@ export default class CustomerController extends ApiController {
}); });
//success //success
this.httpSuccess(response, accessToken); this.httpSuccess(response, {accessToken});
} catch (error) { } catch (error) {
this.httpInternalError(response); this.httpInternalError(response);
return; return;

View File

@ -5,10 +5,12 @@ import { Service } from "typedi";
import AuthService from "@Services/common/AuthService/AuthService"; import AuthService from "@Services/common/AuthService/AuthService";
import { JwtPayload } from "jsonwebtoken"; import { JwtPayload } from "jsonwebtoken";
import IdNotService from "@Services/common/IdNotService/IdNotService";
@Controller() @Controller()
@Service() @Service()
export default class UserController extends ApiController { export default class UserController extends ApiController {
constructor(private authService: AuthService) { constructor(private authService: AuthService, private idNotService: IdNotService) {
super(); super();
} }
@ -19,16 +21,25 @@ export default class UserController extends ApiController {
*/ */
@Post("/api/v1/idnot/user/:code") @Post("/api/v1/idnot/user/:code")
protected async getUserInfosFromIdnot(req: Request, response: Response) { protected async getUserInfosFromIdnot(req: Request, response: Response) {
console.warn("/api/v1/idnot/user/:code used for test, should be removed");
try { try {
const code = req.params["code"]; const code = req.params["code"];
if (!code) throw new Error("code is required"); if (!code) throw new Error("code is required");
const token = await fetch("https://qual-connexion.idnot.fr/IdPOAuth2/token/idnot_idp_v1", { method: "POST" });
console.log(token); const idNotToken = await this.idNotService.getIdNotToken(code);
//const user = await this.authService.getUserFromIdNotTokens(code!); const user = await this.idNotService.getOrCreateUser(idNotToken);
//success
this.httpSuccess(response); if(!user) {
this.httpUnauthorized(response);
return;
}
await this.idNotService.updateUser(user.uid);
await this.idNotService.updateOffice(user.office_uid);
const payload = await this.authService.getUserJwtPayload(user.idNot);
const accessToken = this.authService.generateAccessToken(payload);
const refreshToken = this.authService.generateRefreshToken(payload);
this.httpSuccess(response, { accessToken, refreshToken });
} catch (error) { } catch (error) {
console.log(error); console.log(error);
this.httpInternalError(response); this.httpInternalError(response);
@ -55,7 +66,7 @@ export default class UserController extends ApiController {
} }
} }
@Post("/api/v1/idnot/user/refresh-token") @Post("/api/v1/idnot/user/auth/refresh-token")
protected async refreshToken(req: Request, response: Response) { protected async refreshToken(req: Request, response: Response) {
try { try {
const authHeader = req.headers["authorization"]; const authHeader = req.headers["authorization"];
@ -69,6 +80,7 @@ export default class UserController extends ApiController {
let accessToken; let accessToken;
this.authService.verifyRefreshToken(token, (err, userPayload) => { this.authService.verifyRefreshToken(token, (err, userPayload) => {
if (err) { if (err) {
console.log(err);
this.httpUnauthorized(response); this.httpUnauthorized(response);
return; return;
} }
@ -80,7 +92,7 @@ export default class UserController extends ApiController {
}); });
//success //success
this.httpSuccess(response, accessToken); this.httpSuccess(response, { accessToken });
} catch (error) { } catch (error) {
this.httpInternalError(response); this.httpInternalError(response);
return; return;

View File

@ -0,0 +1,111 @@
import { Response, Request } from "express";
import { Controller, Get, Post } from "@ControllerPattern/index";
import ApiController from "@Common/system/controller-pattern/ApiController";
import { Service } from "typedi";
import { OfficeFolder } from "le-coffre-resources/dist/Notary";
import { getFolderHashes } from "@Common/optics/notary";
import OfficeFoldersService from "@Services/notary/OfficeFoldersService/OfficeFoldersService";
import SecureService from "@Services/common/SecureService/SecureService";
import authHandler from "@App/middlewares/AuthHandler";
import ruleHandler from "@App/middlewares/RulesHandler";
import folderHandler from "@App/middlewares/OfficeMembershipHandlers/FolderHandler";
@Controller()
@Service()
export default class OfficeFoldersController extends ApiController {
constructor(private secureService: SecureService, private officeFoldersService: OfficeFoldersService) {
super();
}
/**
* @description Create a new folder anchor
*/
@Post("/api/v1/notary/anchors/:uid", [authHandler, ruleHandler, folderHandler])
protected async post(req: Request, response: Response) {
try {
const uid = req.params["uid"];
if (!uid) {
this.httpBadRequest(response, "No uid provided");
return;
}
const query = {
documents: {
include: {
files: true,
},
},
};
const officeFolderFound = await this.officeFoldersService.getByUid(uid, query);
if (!officeFolderFound) {
this.httpNotFoundRequest(response, "Office folder not found");
return;
}
const officeFolder = OfficeFolder.hydrate<OfficeFolder>(officeFolderFound, { strategy: "excludeAll" });
const folderHashes = getFolderHashes(officeFolder);
if (folderHashes.length === 0) {
this.httpNotFoundRequest(response, "No file hash to anchor");
return;
}
const sortedHashes = [...folderHashes].sort();
const anchor = await this.secureService.anchor(sortedHashes);
this.httpSuccess(response, anchor);
} catch (error) {
this.httpInternalError(response, error);
return;
}
}
/**
* @description Verify a folder anchor status
*/
@Get("/api/v1/notary/anchors/:uid", [authHandler, ruleHandler, folderHandler])
protected async get(req: Request, response: Response) {
try {
const uid = req.params["uid"];
if (!uid) {
this.httpBadRequest(response, "No uid provided");
return;
}
const query = {
documents: {
include: {
files: true,
},
},
};
const officeFolderFound = await this.officeFoldersService.getByUid(uid, query);
if (!officeFolderFound) {
this.httpNotFoundRequest(response, "Office folder not found");
return;
}
const officeFolder = OfficeFolder.hydrate<OfficeFolder>(officeFolderFound, { strategy: "excludeAll" });
const folderHashes = getFolderHashes(officeFolder);
if (folderHashes.length === 0) {
this.httpNotFoundRequest(response, "No file hash to anchor");
return;
}
const sortedHashes = [...folderHashes].sort();
const anchor = await this.secureService.verify(sortedHashes);
this.httpSuccess(response, anchor);
} catch (error) {
this.httpInternalError(response, error);
return;
}
}
}

View File

@ -3,7 +3,7 @@ import { Controller, Get } from "@ControllerPattern/index";
import ApiController from "@Common/system/controller-pattern/ApiController"; import ApiController from "@Common/system/controller-pattern/ApiController";
import UsersService from "@Services/notary/UsersService/UsersService"; import UsersService from "@Services/notary/UsersService/UsersService";
import { Service } from "typedi"; import { Service } from "typedi";
import User from "le-coffre-resources/dist/SuperAdmin"; import User from "le-coffre-resources/dist/Notary/User";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import authHandler from "@App/middlewares/AuthHandler"; import authHandler from "@App/middlewares/AuthHandler";
import ruleHandler from "@App/middlewares/RulesHandler"; import ruleHandler from "@App/middlewares/RulesHandler";
@ -34,7 +34,7 @@ export default class UsersController extends ApiController {
//call service to get prisma entity //call service to get prisma entity
const usersEntities = await this.usersService.get(query); const usersEntities = await this.usersService.get(query);
//Hydrate ressource with prisma entity //Hydrate ressource with prisma entity
const users = User.hydrateArray<User>(usersEntities, { strategy: "excludeAll" }); const users = User.hydrateArray<User>(usersEntities, { strategy: "excludeAll" });

View File

@ -41,10 +41,12 @@ import OfficeRolesControllerNotary from "./api/notary/OfficeRolesController";
import FilesControllerCustomer from "./api/customer/FilesController"; import FilesControllerCustomer from "./api/customer/FilesController";
import DocumentsControllerCustomer from "./api/customer/DocumentsController"; import DocumentsControllerCustomer from "./api/customer/DocumentsController";
import OfficeFoldersController from "./api/customer/OfficeFoldersController"; import OfficeFoldersController from "./api/customer/OfficeFoldersController";
import OfficeFolderAnchorsController from "./api/notary/OfficeFolderAnchorsController";
import CustomersController from "./api/customer/CustomersController"; import CustomersController from "./api/customer/CustomersController";
import AppointmentsController from "./api/super-admin/AppointmentsController"; import AppointmentsController from "./api/super-admin/AppointmentsController";
import VotesController from "./api/super-admin/VotesController"; import VotesController from "./api/super-admin/VotesController";
import LiveVoteController from "./api/super-admin/LiveVoteController"; import LiveVoteController from "./api/super-admin/LiveVoteController";
import UserNotificationController from "./api/common/UserNotificationController"; import UserNotificationController from "./api/common/UserNotificationController";
@ -52,6 +54,7 @@ import UserNotificationController from "./api/common/UserNotificationController"
* @description This allow to declare all controllers used in the application * @description This allow to declare all controllers used in the application
*/ */
export default { export default {
start: () => { start: () => {
Container.get(HomeController); Container.get(HomeController);
Container.get(UsersControllerSuperAdmin); Container.get(UsersControllerSuperAdmin);
@ -99,6 +102,7 @@ export default {
Container.get(FilesControllerCustomer); Container.get(FilesControllerCustomer);
Container.get(DocumentsControllerCustomer); Container.get(DocumentsControllerCustomer);
Container.get(OfficeFoldersController); Container.get(OfficeFoldersController);
Container.get(OfficeFolderAnchorsController);
Container.get(CustomersController); Container.get(CustomersController);
Container.get(UserNotificationController); Container.get(UserNotificationController);
}, },

View File

@ -25,6 +25,9 @@ export class BackendVariables {
@IsNotEmpty() @IsNotEmpty()
public readonly API_ROOT_URL!: string; public readonly API_ROOT_URL!: string;
@IsNotEmpty()
public readonly APP_HOST!: string;
@IsOptional() @IsOptional()
public readonly APP_LABEL!: string; public readonly APP_LABEL!: string;
@ -34,6 +37,12 @@ export class BackendVariables {
@IsNotEmpty() @IsNotEmpty()
public readonly APP_ROOT_URL!: string; public readonly APP_ROOT_URL!: string;
@IsNotEmpty()
public readonly IDNOT_BASE_URL!: string;
@IsNotEmpty()
public readonly IDNOT_API_BASE_URL!: string;
@IsNotEmpty() @IsNotEmpty()
public readonly IDNOT_CONNEXION_URL!: string; public readonly IDNOT_CONNEXION_URL!: string;
@ -46,6 +55,9 @@ export class BackendVariables {
@IsNotEmpty() @IsNotEmpty()
public readonly IDNOT_REDIRECT_URL!: string; public readonly IDNOT_REDIRECT_URL!: string;
@IsNotEmpty()
public readonly IDNOT_API_KEY!: string;
@IsNotEmpty() @IsNotEmpty()
public readonly PINATA_API_KEY!: string; public readonly PINATA_API_KEY!: string;
@ -64,11 +76,16 @@ export class BackendVariables {
@IsNotEmpty() @IsNotEmpty()
public readonly MAILCHIMP_API_KEY!: string; public readonly MAILCHIMP_API_KEY!: string;
@IsNotEmpty()
public readonly SECURE_API_KEY!: string;
@IsNotEmpty()
public readonly SECURE_API_BASE_URL!: string;
@IsNotEmpty() @IsNotEmpty()
public readonly ENV!: string; public readonly ENV!: string;
public constructor() { public constructor() {
dotenv.config(); dotenv.config();
this.DATABASE_PORT = process.env["DATABASE_PORT"]!; this.DATABASE_PORT = process.env["DATABASE_PORT"]!;
this.DATABASE_HOST = process.env["DATABASE_HOST"]!; this.DATABASE_HOST = process.env["DATABASE_HOST"]!;
@ -77,29 +94,34 @@ export class BackendVariables {
this.DATABASE_NAME = process.env["DATABASE_NAME"]!; this.DATABASE_NAME = process.env["DATABASE_NAME"]!;
this.DATABASE_URL = process.env["DEV_PRISMA_STUDIO_DB_URL"]!; this.DATABASE_URL = process.env["DEV_PRISMA_STUDIO_DB_URL"]!;
this.API_ROOT_URL = process.env["API_ROOT_URL"]!; this.API_ROOT_URL = process.env["API_ROOT_URL"]!;
this.APP_HOST = process.env["APP_HOST"]!;
this.APP_PORT = process.env["APP_PORT"]!; this.APP_PORT = process.env["APP_PORT"]!;
this.APP_ROOT_URL = process.env["APP_ROOT_URL"]!; this.APP_ROOT_URL = process.env["APP_ROOT_URL"]!;
this.APP_LABEL = process.env["APP_LABEL"]!; this.APP_LABEL = process.env["APP_LABEL"]!;
this.IDNOT_BASE_URL = process.env["IDNOT_BASE_URL"]!;
this.IDNOT_API_BASE_URL = process.env["IDNOT_API_BASE_URL"]!;
this.IDNOT_CONNEXION_URL = process.env["IDNOT_CONNEXION_URL"]!; this.IDNOT_CONNEXION_URL = process.env["IDNOT_CONNEXION_URL"]!;
this.IDNOT_CLIENT_ID = process.env["IDNOT_CLIENT_ID"]!; this.IDNOT_CLIENT_ID = process.env["IDNOT_CLIENT_ID"]!;
this.IDNOT_CLIENT_SECRET = process.env["IDNOT_CLIENT_SECRET"]!; this.IDNOT_CLIENT_SECRET = process.env["IDNOT_CLIENT_SECRET"]!;
this.IDNOT_REDIRECT_URL = process.env["IDNOT_REDIRECT_URL"]!; this.IDNOT_REDIRECT_URL = process.env["IDNOT_REDIRECT_URL"]!;
this.IDNOT_API_KEY = process.env["IDNOT_API_KEY"]!;
this.PINATA_API_KEY = process.env["PINATA_API_KEY"]!; this.PINATA_API_KEY = process.env["PINATA_API_KEY"]!;
this.PINATA_API_SECRET = process.env["PINATA_API_SECRET"]!; this.PINATA_API_SECRET = process.env["PINATA_API_SECRET"]!;
this.PINATA_GATEWAY = process.env["PINATA_GATEWAY"]!; this.PINATA_GATEWAY = process.env["PINATA_GATEWAY"]!;
this.ACCESS_TOKEN_SECRET = process.env["ACCESS_TOKEN_SECRET"]!; this.ACCESS_TOKEN_SECRET = process.env["ACCESS_TOKEN_SECRET"]!;
this.REFRESH_TOKEN_SECRET = process.env["REFRESH_TOKEN_SECRET"]!; this.REFRESH_TOKEN_SECRET = process.env["REFRESH_TOKEN_SECRET"]!;
this.MAILCHIMP_API_KEY = process.env["MAILCHIMP_API_KEY"]!; this.MAILCHIMP_API_KEY = process.env["MAILCHIMP_API_KEY"]!;
this.SECURE_API_KEY = process.env["SECURE_API_KEY"]!;
this.SECURE_API_BASE_URL = process.env["SECURE_API_BASE_URL"]!;
this.ENV = process.env["ENV"]!; this.ENV = process.env["ENV"]!;
} }
public async validate(groups?: string[]) { public async validate(groups?: string[]) {
const validationOptions = groups ? { groups } : undefined; const validationOptions = groups ? { groups } : undefined;
try { try {
await validateOrReject(this, validationOptions); await validateOrReject(this, validationOptions);
} catch (error: any) { } catch (error: any) {
if (process.env["ENV"] === "dev") { if (process.env["ENV"] === "dev" || process.env["ENV"] === 'stg') {
throw error; throw error;
} }
throw new Error("Some env variables are required!"); throw new Error("Some env variables are required!");

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "offices" ADD COLUMN "checked_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "users" ADD COLUMN "checked_at" TIMESTAMP(3);

View File

@ -0,0 +1,71 @@
/*
Warnings:
- The values [ANCHORED] on the enum `EDocumentStatus` will be removed. If these variants are still used in the database, this will fail.
- You are about to drop the column `blockchain_anchor_uid` on the `documents` table. All the data in the column will be lost.
- You are about to drop the `blockchain_anchors` table. If the table is not empty, all the data it contains will be lost.
- A unique constraint covering the columns `[folder_anchor_uid]` on the table `office_folders` will be added. If there are existing duplicate values, this will fail.
- Added the required column `hash` to the `files` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "EBlockchainName" AS ENUM ('TEZOS');
-- CreateEnum
CREATE TYPE "EAnchoringStatus" AS ENUM ('QUEUED', 'ATTEMPTING', 'VERIFIED_ON_CHAIN', 'VERIFYING_ON_CHAIN', 'ABANDONED');
-- AlterEnum
BEGIN;
CREATE TYPE "EDocumentStatus_new" AS ENUM ('ASKED', 'DEPOSITED', 'VALIDATED', 'REFUSED');
ALTER TABLE "documents" ALTER COLUMN "document_status" DROP DEFAULT;
ALTER TABLE "document_history" ALTER COLUMN "document_status" DROP DEFAULT;
ALTER TABLE "documents" ALTER COLUMN "document_status" TYPE "EDocumentStatus_new" USING ("document_status"::text::"EDocumentStatus_new");
ALTER TABLE "document_history" ALTER COLUMN "document_status" TYPE "EDocumentStatus_new" USING ("document_status"::text::"EDocumentStatus_new");
ALTER TYPE "EDocumentStatus" RENAME TO "EDocumentStatus_old";
ALTER TYPE "EDocumentStatus_new" RENAME TO "EDocumentStatus";
DROP TYPE "EDocumentStatus_old";
ALTER TABLE "documents" ALTER COLUMN "document_status" SET DEFAULT 'ASKED';
ALTER TABLE "document_history" ALTER COLUMN "document_status" SET DEFAULT 'ASKED';
COMMIT;
-- DropForeignKey
ALTER TABLE "documents" DROP CONSTRAINT "documents_blockchain_anchor_uid_fkey";
-- AlterTable
ALTER TABLE "documents" DROP COLUMN "blockchain_anchor_uid";
-- AlterTable
ALTER TABLE "files" ADD COLUMN "hash" VARCHAR(255) NOT NULL;
-- AlterTable
ALTER TABLE "office_folders" ADD COLUMN "folder_anchor_uid" VARCHAR(255);
-- DropTable
DROP TABLE "blockchain_anchors";
-- CreateTable
CREATE TABLE "office_folder_anchors" (
"uid" TEXT NOT NULL,
"hash_sources" TEXT[],
"root_hash" VARCHAR(255) NOT NULL,
"blockchain" "EBlockchainName" NOT NULL DEFAULT 'TEZOS',
"status" "EAnchoringStatus" NOT NULL DEFAULT 'QUEUED',
"anchor_nb_try" INTEGER NOT NULL DEFAULT 0,
"anchored_at" TIMESTAMP(3),
"tx_id" VARCHAR(255),
"tx_link" VARCHAR(255),
"tx_hash" VARCHAR(255),
"created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3),
CONSTRAINT "office_folder_anchors_pkey" PRIMARY KEY ("uid")
);
-- CreateIndex
CREATE UNIQUE INDEX "office_folder_anchors_uid_key" ON "office_folder_anchors"("uid");
-- CreateIndex
CREATE UNIQUE INDEX "office_folders_folder_anchor_uid_key" ON "office_folders"("folder_anchor_uid");
-- AddForeignKey
ALTER TABLE "office_folders" ADD CONSTRAINT "office_folders_folder_anchor_uid_fkey" FOREIGN KEY ("folder_anchor_uid") REFERENCES "office_folder_anchors"("uid") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -61,6 +61,7 @@ model Users {
office_role_uid String? @db.VarChar(255) office_role_uid String? @db.VarChar(255)
created_at DateTime? @default(now()) created_at DateTime? @default(now())
updated_at DateTime? @updatedAt updated_at DateTime? @updatedAt
checked_at DateTime?
office_membership Offices @relation(fields: [office_uid], references: [uid], onDelete: Cascade) office_membership Offices @relation(fields: [office_uid], references: [uid], onDelete: Cascade)
office_uid String @db.VarChar(255) office_uid String @db.VarChar(255)
office_folders OfficeFolders[] @relation("OfficeFolderHasStakeholders") office_folders OfficeFolders[] @relation("OfficeFolderHasStakeholders")
@ -81,6 +82,7 @@ model Offices {
office_status EOfficeStatus @default(DESACTIVATED) office_status EOfficeStatus @default(DESACTIVATED)
created_at DateTime? @default(now()) created_at DateTime? @default(now())
updated_at DateTime? @updatedAt updated_at DateTime? @updatedAt
checked_at DateTime?
deed_types DeedTypes[] deed_types DeedTypes[]
users Users[] users Users[]
office_folders OfficeFolders[] office_folders OfficeFolders[]
@ -142,17 +144,41 @@ model OfficeFolders {
customers Customers[] @relation("OfficeFolderHasCustomers") customers Customers[] @relation("OfficeFolderHasCustomers")
documents Documents[] documents Documents[]
folder_anchor OfficeFolderAnchors? @relation(fields: [folder_anchor_uid], references: [uid])
folder_anchor_uid String? @unique @db.VarChar(255)
@@unique([folder_number, office_uid]) @@unique([folder_number, office_uid])
@@map("office_folders") @@map("office_folders")
} }
model OfficeFolderAnchors {
uid String @id @unique @default(uuid())
hash_sources String[]
root_hash String @db.VarChar(255)
blockchain EBlockchainName @default(TEZOS)
status EAnchoringStatus @default(QUEUED)
anchor_nb_try Int @default(0)
anchored_at DateTime?
tx_id String? @db.VarChar(255)
tx_link String? @db.VarChar(255)
tx_hash String? @db.VarChar(255)
folder OfficeFolders?
created_at DateTime? @default(now())
updated_at DateTime? @updatedAt
@@map("office_folder_anchors")
}
model Documents { model Documents {
uid String @id @unique @default(uuid()) uid String @id @unique @default(uuid())
document_status EDocumentStatus @default(ASKED) document_status EDocumentStatus @default(ASKED)
document_type DocumentTypes @relation(fields: [document_type_uid], references: [uid]) document_type DocumentTypes @relation(fields: [document_type_uid], references: [uid])
document_type_uid String @db.VarChar(255) document_type_uid String @db.VarChar(255)
blockchain_anchor BlockchainAnchors? @relation(fields: [blockchain_anchor_uid], references: [uid])
blockchain_anchor_uid String? @db.VarChar(255)
folder OfficeFolders @relation(fields: [folder_uid], references: [uid]) folder OfficeFolders @relation(fields: [folder_uid], references: [uid])
folder_uid String @db.VarChar(255) folder_uid String @db.VarChar(255)
depositor Customers @relation(fields: [depositor_uid], references: [uid], onDelete: Cascade) depositor Customers @relation(fields: [depositor_uid], references: [uid], onDelete: Cascade)
@ -184,6 +210,7 @@ model Files {
file_path String @unique @db.VarChar(255) file_path String @unique @db.VarChar(255)
file_name String @db.VarChar(255) file_name String @db.VarChar(255)
mimetype String @db.VarChar(255) mimetype String @db.VarChar(255)
hash String @db.VarChar(255)
size Int size Int
archived_at DateTime? archived_at DateTime?
key String? @db.VarChar(255) key String? @db.VarChar(255)
@ -193,16 +220,6 @@ model Files {
@@map("files") @@map("files")
} }
model BlockchainAnchors {
uid String @id @unique @default(uuid())
smartSigJobId String @unique @db.VarChar(255)
created_at DateTime? @default(now())
updated_at DateTime? @updatedAt
documents Documents[]
@@map("blockchain_anchors")
}
model DocumentTypes { model DocumentTypes {
uid String @id @unique @default(uuid()) uid String @id @unique @default(uuid())
name String @db.VarChar(255) name String @db.VarChar(255)
@ -355,7 +372,6 @@ enum EDocumentStatus {
ASKED ASKED
DEPOSITED DEPOSITED
VALIDATED VALIDATED
ANCHORED
REFUSED REFUSED
} }
@ -368,3 +384,15 @@ enum EAppointmentStatus {
OPEN OPEN
CLOSED CLOSED
} }
enum EBlockchainName {
TEZOS
}
enum EAnchoringStatus {
QUEUED
ATTEMPTING
VERIFIED_ON_CHAIN
VERIFYING_ON_CHAIN
ABANDONED
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
import * as Optics from "monocle-ts";
import * as Traversal from "monocle-ts/Traversal";
import * as Array from "fp-ts/Array";
import { Document, File, OfficeFolder } from "le-coffre-resources/dist/Notary";
/**
* Lenses
*/
export const folderDocumentsLens = Optics.Lens.fromNullableProp<OfficeFolder>()("documents", []);
export const documentFilesLens = Optics.Lens.fromNullableProp<Document>()("files", []);
export const fileHashLens = Optics.Lens.fromProp<File>()("hash");
/**
* Traversals
*/
export const documentsTraversal = Optics.fromTraversable(Array.Traversable)<Document>();
export const filesTraversal = Optics.fromTraversable(Array.Traversable)<File>();
export const folderHashesTraversal = folderDocumentsLens
.composeTraversal(documentsTraversal)
.composeLens(documentFilesLens)
.composeTraversal(filesTraversal)
.composeLens(fileHashLens);
/**
* Getters
*/
export const getFolderHashes = (folder: OfficeFolder) => Traversal.getAll(folder)(folderHashesTraversal);

View File

@ -38,6 +38,7 @@ export default class FilesRepository extends BaseRepository {
file_name: file.file_name, file_name: file.file_name,
file_path: file.file_path, file_path: file.file_path,
mimetype: file.mimetype, mimetype: file.mimetype,
hash: file.hash,
size: file.size, size: file.size,
key: key, key: key,
}, },
@ -57,6 +58,7 @@ export default class FilesRepository extends BaseRepository {
file_name: file.file_name, file_name: file.file_name,
file_path: file.file_path, file_path: file.file_path,
mimetype: file.mimetype, mimetype: file.mimetype,
hash: file.hash,
size: file.size, size: file.size,
key: key, key: key,
}, },

View File

@ -72,6 +72,31 @@ export default class OfficesRepository extends BaseRepository {
return this.model.update(updateArgs); return this.model.update(updateArgs);
} }
/**
* @description : Update check date of an office
*/
public async updateCheckedAt(uid: string) {
return this.model.update({
where: {
uid: uid,
},
data: {
checked_at: new Date(),
},
});
}
/**
* @description : Delete an office
*/
public async delete(uid: string): Promise<Offices> {
return this.model.delete({
where: {
uid: uid,
},
});
}
/** /**
* @description : Find one office * @description : Find one office
*/ */
@ -93,4 +118,25 @@ export default class OfficesRepository extends BaseRepository {
include: query, include: query,
}); });
} }
/**
* @description : Find offices which need to be checked with idNot API
*/
public async findManyToCheck() {
return this.model.findMany({
where: {
OR: [
{
checked_at: null,
},
{
checked_at: { lt: new Date(Date.now() - 3 * 60 * 1000) }, // less than 24h ago
},
],
},
include: {
address: true,
},
});
}
} }

View File

@ -87,7 +87,7 @@ export default class UsersRepository extends BaseRepository {
/** /**
* @description : Update data from a user * @description : Update data from a user
*/ */
public async update(uid: string, user: User): Promise<Users> { public async update(uid: string, user: User): Promise<Users> {
const updateArgs: Prisma.UsersUpdateArgs = { const updateArgs: Prisma.UsersUpdateArgs = {
where: { where: {
@ -108,7 +108,7 @@ export default class UsersRepository extends BaseRepository {
}, },
}; };
if(user.office_membership) { if (user.office_membership) {
updateArgs.data.office_membership = { updateArgs.data.office_membership = {
connect: { connect: {
idNot: user.office_membership?.idNot, idNot: user.office_membership?.idNot,
@ -124,7 +124,7 @@ export default class UsersRepository extends BaseRepository {
}; };
} }
if(user.office_role) { if (user.office_role) {
updateArgs.data.office_role = { updateArgs.data.office_role = {
connect: { connect: {
uid: user.office_role.uid, uid: user.office_role.uid,
@ -132,7 +132,7 @@ export default class UsersRepository extends BaseRepository {
}; };
} }
if(user.role) { if (user.role) {
updateArgs.data.role = { updateArgs.data.role = {
connect: { connect: {
uid: user.role.uid, uid: user.role.uid,
@ -144,17 +144,31 @@ export default class UsersRepository extends BaseRepository {
} }
/** /**
* @description : Update role from a user * @description : Update check date of a user
*/ */
public async updateRole(uid: string, user: User): Promise<Users> { public async updateCheckedAt(uid: string) {
return this.model.update({
where: {
uid: uid,
},
data: {
checked_at: new Date(),
},
});
}
/**
* @description : Update office role from a user
*/
public async updateOfficeRole(uid: string, user: User): Promise<Users> {
const updateArgs: Prisma.UsersUpdateArgs = { const updateArgs: Prisma.UsersUpdateArgs = {
where: { where: {
uid: uid, uid: uid,
}, },
data: {} data: {},
}; };
if(user.office_role) { if (user.office_role) {
updateArgs.data.office_role = { updateArgs.data.office_role = {
connect: { connect: {
uid: user.office_role.uid, uid: user.office_role.uid,
@ -162,17 +176,20 @@ export default class UsersRepository extends BaseRepository {
}; };
} }
if(user.role) {
updateArgs.data.role = {
connect: {
uid: user.role.uid,
},
};
}
return this.model.update({ ...updateArgs, include: { contact: true, office_membership: { include: { address: true } } } }); return this.model.update({ ...updateArgs, include: { contact: true, office_membership: { include: { address: true } } } });
} }
/**
* @description : Delete one user
*/
public async delete(uid: string): Promise<Users> {
return this.model.delete({
where: {
uid: uid,
},
});
}
/** /**
* @description : Find one user * @description : Find one user
*/ */
@ -200,7 +217,7 @@ export default class UsersRepository extends BaseRepository {
/** /**
* @description : Find one user with office * @description : Find one user with office
*/ */
public async findOneByUidWithRole(uid: string): Promise<((Users & {role: Role} )| null)> { public async findOneByUidWithRole(uid: string): Promise<(Users & { role: Role }) | null> {
return this.model.findUnique({ return this.model.findUnique({
where: { where: {
uid: uid, uid: uid,
@ -222,4 +239,27 @@ export default class UsersRepository extends BaseRepository {
}, },
}); });
} }
/**
* @description : Find users which need to be checked with idNot API
*/
public async findManyToCheck() {
return this.model.findMany({
where: {
OR: [
{
checked_at: null,
},
{
checked_at: { lt: new Date(Date.now() - 3 * 60 * 1000) }, // less than 24h ago
},
],
},
include: {
contact: true,
role: true,
office_membership: true,
},
});
}
} }

View File

@ -7,7 +7,9 @@ import CronService from "@Services/common/CronService/CronService";
(async () => { (async () => {
try { try {
const variables = await Container.get(BackendVariables).validate(); const variables = await Container.get(BackendVariables).validate();
if(variables.ENV === "stg"){ Container.get(CronService).archiveFiles();
await Container.get(CronService).updateUsers();
if(variables.ENV !== "dev"){
Container.get(CronService).sendMails(); Container.get(CronService).sendMails();
} }
} catch (e) { } catch (e) {

View File

@ -19,20 +19,12 @@ export default class UsersService extends BaseService {
return this.userRepository.findMany(query); return this.userRepository.findMany(query);
} }
/**
* @description : Create a user
* @throws {Error} If user couldn't be created
*/
public create(userEntity: User): Promise<Users> {
return this.userRepository.create(userEntity);
}
/** /**
* @description : Modify a user * @description : Modify a user
* @throws {Error} If user modification failed * @throws {Error} If user modification failed
*/ */
public async update(uid: string, userEntity: User): Promise<Users> { public async update(uid: string, userEntity: User): Promise<Users> {
return this.userRepository.updateRole(uid, userEntity); return this.userRepository.updateOfficeRole(uid, userEntity);
} }
/** /**

View File

@ -15,7 +15,12 @@ interface ICustomerJwtPayload {
customerId: string; customerId: string;
email: string; email: string;
} }
export interface IdNotJwtPayload {
sub: string,
profile_idn: string,
entity_idn: string,
}
interface IUserJwtPayload { interface IUserJwtPayload {
userId: string; userId: string;
@ -79,7 +84,7 @@ export default class AuthService extends BaseService {
}; };
} }
public generateAccessToken(user: any): string { public generateAccessToken(user: any): string {
return jwt.sign({ ...user }, this.variables.ACCESS_TOKEN_SECRET, { expiresIn: "1h" }); return jwt.sign({ ...user }, this.variables.ACCESS_TOKEN_SECRET, { expiresIn: "15m" });
} }
public generateRefreshToken(user: any): string { public generateRefreshToken(user: any): string {

View File

@ -1,14 +1,16 @@
import { Service } from "typedi"; import { Service } from "typedi";
import { CronJob } from "cron"; import { CronJob } from "cron";
import MailchimpService from "../MailchimpService/MailchimpService"; import MailchimpService from "../MailchimpService/MailchimpService";
import FilesService from "../FilesService/FilesService";
import IdNotService from "../IdNotService/IdNotService";
@Service() @Service()
export default class CronService { export default class CronService {
constructor(private mailchimpService: MailchimpService) {} constructor(private mailchimpService: MailchimpService, private filesService: FilesService, private idNotService: IdNotService) {}
public async sendMails() { public async sendMails() {
const cronJob = new CronJob("*/15 * * * * *", async () => { const cronJob = new CronJob("*/15 * * * *", async () => { // Every 15 minutes
try { try {
await this.mailchimpService.sendEmails(); await this.mailchimpService.sendEmails();
} catch (e) { } catch (e) {
@ -21,4 +23,33 @@ export default class CronService {
cronJob.start(); cronJob.start();
} }
} }
public async archiveFiles() {
const cronJob = new CronJob("0 0 * * MON", async () => { // Every monday at midnight
try {
await this.filesService.archiveOldFiles();
} catch (e) {
console.error(e);
}
});
// Start job
if (!cronJob.running) {
cronJob.start();
}
}
public async updateUsers() {
const cronJob = new CronJob("0 0 * * *", async () => { // Once a day at midnight
try {
await this.idNotService.updateOffices();
await this.idNotService.updateUsers();
} catch (e) {
console.error(e);
}
});
// Start job
if (!cronJob.running) {
cronJob.start();
}
}
} }

View File

@ -15,6 +15,10 @@ export default class CryptoService extends BaseService {
return crypto.createHash("sha256").update(String(key)).digest("base64").slice(0, 32); return crypto.createHash("sha256").update(String(key)).digest("base64").slice(0, 32);
} }
public async getHash(buffer: Buffer): Promise<string> {
return crypto.createHash("sha256").update(buffer).digest("hex");
}
/** /**
* @description : encrypt data * @description : encrypt data
* @throws {Error} If data cannot be encrypted * @throws {Error} If data cannot be encrypted

View File

@ -71,12 +71,15 @@ export default class FilesService extends BaseService {
public async create(file: File, fileData: Express.Multer.File) { public async create(file: File, fileData: Express.Multer.File) {
const key = v4(); const key = v4();
const encryptedFile = await this.cryptoService.encrypt(fileData.buffer, key); const encryptedFile = await this.cryptoService.encrypt(fileData.buffer, key);
const hash = await this.cryptoService.getHash(fileData.buffer);
const upload = await this.ipfsService.pinFile(Readable.from(encryptedFile), fileData.originalname); const upload = await this.ipfsService.pinFile(Readable.from(encryptedFile), fileData.originalname);
let fileToCreate: File = file; let fileToCreate: File = file;
fileToCreate.file_name = fileData.originalname; fileToCreate.file_name = fileData.originalname;
fileToCreate.file_path = this.variables.PINATA_GATEWAY.concat(upload.IpfsHash); fileToCreate.file_path = this.variables.PINATA_GATEWAY.concat(upload.IpfsHash);
fileToCreate.mimetype = fileData.mimetype; fileToCreate.mimetype = fileData.mimetype;
fileToCreate.size = fileData.size; fileToCreate.size = fileData.size;
fileToCreate.hash = hash;
fileToCreate.archived_at = null; fileToCreate.archived_at = null;
return this.filesRepository.create(fileToCreate, key); return this.filesRepository.create(fileToCreate, key);
@ -107,4 +110,32 @@ export default class FilesService extends BaseService {
return this.filesRepository.deleteKeyAndArchive(uid); return this.filesRepository.deleteKeyAndArchive(uid);
} }
/**
* @description : find files to be archived
* @throws {Error} If file key cannot be deleted or archived
*/
public async getFilesToBeArchived() {
return this.filesRepository.findMany({
where: {
archived_at: null,
created_at: { lt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30 * 3) }, // 90 days
},
});
}
/**
* @description : find files to be archived
* @throws {Error} If file key cannot be deleted or archived
*/
public async archiveOldFiles() {
const files = await this.getFilesToBeArchived();
files.forEach(async (file) => {
try {
await this.deleteKeyAndArchive(file.uid);
} catch (error) {
console.error(error);
}
});
}
} }

View File

@ -0,0 +1,371 @@
import BaseService from "@Services/BaseService";
import { BackendVariables } from "@Common/config/variables/Variables";
import { Service } from "typedi";
import jwt from "jsonwebtoken";
import UsersService from "@Services/super-admin/UsersService/UsersService";
import { IdNotJwtPayload } from "../AuthService/AuthService";
import User, { Office, Role } from "le-coffre-resources/dist/SuperAdmin";
import RolesService from "@Services/super-admin/RolesService/RolesService";
import OfficesService from "@Services/super-admin/OfficesService/OfficesService";
import { EOfficeStatus } from "@prisma/client";
interface IIdNotToken {
access_token: string;
expires_in: number;
id_token: string;
token_type: string;
}
interface IRattachementData {
entiteUrl: string;
personneUrl: string;
entite: {
codeCrpcen: string;
typeEntite: {
name: string;
};
ou: string;
denominationSociale: string;
statutEntite: {
name: string;
};
locationsUrl: string;
}
personne: {
numeroTelephonePro: string;
prenom: string;
nomUsuel: string;
mobilePro: string;
numeroAdherentCrpcen: string;
civilite: string;
};
typeLien: {
name: string;
};
numeroMobile: string;
numeroTelephone: string;
statutDuRattachement: boolean;
mailRattachement: string;
}
interface IOfficeData {
ou: string;
intitule: string;
denominationSociale: string;
departementResidence: {
libelle: string;
code: string;
}[];
libelle: string;
codeCrpcen: string;
locationsUrl: string;
statutEntite: {
name: string;
};
typeEntite: {
name: string;
}
}
interface IOfficeLocation {
totalResultCount: number;
page: number;
size: number;
result: {
mail: string;
numeroTelephone: string;
adrGeo4: string;
adrGeoCodePostal: string;
adrGeoVille: string;
ou: string;
lastModified: string;
deleted: boolean;
}[];
}
enum EIdnotRole {
DIRECTEUR = "Directeur général du CSN",
NOTAIRE_TITULAIRE = "Notaire titulaire",
NOTAIRE_ASSOCIE = "Notaire associé",
NOTAIRE_SALARIE = "Notaire salarié",
COLLABORATEUR = "Collaborateur",
SECRETAIRE_GENERAL = "Secrétaire général",
SUPPLEANT = "Suppléant",
ADMINISTRATEUR = "Administrateur",
RESPONSABLE = "Responsable",
}
@Service()
export default class IdNotService extends BaseService {
constructor(
protected variables: BackendVariables,
private userService: UsersService,
private officeService: OfficesService,
private rolesService: RolesService,
) {
super();
}
public async getIdNotToken(code: string) {
const query = new URLSearchParams({
client_id: this.variables.IDNOT_CLIENT_ID,
client_secret: this.variables.IDNOT_CLIENT_SECRET,
redirect_uri: `${this.variables.APP_HOST}/authorized-client`,
code: code,
grant_type: "authorization_code",
});
const token = await fetch(this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL + "?" + query, { method: "POST" });
const decodedToken = (await token.json()) as IIdNotToken;
const decodedIdToken = jwt.decode(decodedToken.id_token) as IdNotJwtPayload;
return decodedIdToken;
}
public async getRole(roleName: string): Promise<Role> {
switch (roleName) {
case EIdnotRole.DIRECTEUR:
return (await this.rolesService.get({ where: { name: "admin" } }))[0]!;
case EIdnotRole.NOTAIRE_TITULAIRE:
return (await this.rolesService.get({ where: { name: "notary" } }))[0]!;
case EIdnotRole.NOTAIRE_ASSOCIE:
return (await this.rolesService.get({ where: { name: "notary" } }))[0]!;
case EIdnotRole.NOTAIRE_SALARIE:
return (await this.rolesService.get({ where: { name: "notary" } }))[0]!;
case EIdnotRole.COLLABORATEUR:
return (await this.rolesService.get({ where: { name: "collaborator" } }))[0]!;
case EIdnotRole.SECRETAIRE_GENERAL:
return (await this.rolesService.get({ where: { name: "notary" } }))[0]!;
case EIdnotRole.SUPPLEANT:
return (await this.rolesService.get({ where: { name: "notary" } }))[0]!;
case EIdnotRole.ADMINISTRATEUR:
return (await this.rolesService.get({ where: { name: "admin" } }))[0]!;
case EIdnotRole.RESPONSABLE:
return (await this.rolesService.get({ where: { name: "collaborator" } }))[0]!;
default:
return (await this.rolesService.get({ where: { name: "default" } }))[0]!;
}
}
public getOfficeStatus(statusName: string) {
switch (statusName) {
case "Pourvu":
return EOfficeStatus.ACTIVATED;
case "Pourvu mais décédé":
return EOfficeStatus.ACTIVATED;
case "Sans titulaire":
return EOfficeStatus.ACTIVATED;
case "Vacance":
return EOfficeStatus.ACTIVATED;
case "En activité":
return EOfficeStatus.ACTIVATED;
default:
return EOfficeStatus.DESACTIVATED;
}
}
public async updateUser(userId: string) {
const userInfos = await this.userService.getByUid(userId, { contact: true, role: true, office_membership: true });
const user = User.hydrate<User>(userInfos!);
const searchParams = new URLSearchParams({
key: this.variables.IDNOT_API_KEY,
});
let userData = await (await fetch(
`${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/rattachements/${user.idNot}_${user.office_membership!.idNot}?` +
searchParams,
{
method: "GET",
},
)).json() as IRattachementData;
if (!userData.statutDuRattachement) {
const rattachements = await (await fetch(
`${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/personnes/${user.idNot}/rattachements?` +
searchParams,
{
method: "GET",
},
)).json() as any;
if (rattachements.totalResultCount === 0) {
await this.userService.updateCheckedAt(user.uid!);
//await this.userService.delete(user.uid!);
return;
}
const rattachementsResults = rattachements.result as IRattachementData[];
if(!rattachementsResults) return;
rattachementsResults.forEach(async (rattachement) => {
if (rattachement.statutDuRattachement) {
const officeData = await (await fetch(
`${this.variables.IDNOT_API_BASE_URL + rattachement.entiteUrl}?` +
searchParams,
{
method: "GET",
},
)).json() as IOfficeData;
if(officeData.typeEntite.name === "office") {
userData = rattachement;
}
}
});
}
const roleFromIdNot = await this.getRole(userData.typeLien.name);
let updates = 0;
if (user.role!.uid !== roleFromIdNot.uid) {
updates++;
user.role = roleFromIdNot;
}
if (user.office_membership!.idNot !== userData.entite.ou) {
updates++;
let officeData = (await this.officeService.get({ where: { idNot:userData.entite.ou } }))[0];
if (!officeData) {
const officeLocationData = (await (
await fetch(`${this.variables.IDNOT_API_BASE_URL + userData.entite.locationsUrl}?` + searchParams, { method: "GET" })
).json()) as IOfficeLocation;
const office = {
idNot: userData.entite.ou,
name: userData.entite.denominationSociale,
crpcen: userData.entite.codeCrpcen,
office_status: this.getOfficeStatus(userData.entite.statutEntite.name),
address: {
address: officeLocationData.result[0]!.adrGeo4,
city: officeLocationData.result[0]!.adrGeoVille.split(" ")[0] ?? officeLocationData.result[0]!.adrGeoVille,
zip_code: Number(officeLocationData.result[0]!.adrGeoCodePostal),
created_at: null,
updated_at: null,
},
created_at: null,
updated_at: null,
};
officeData = await this.officeService.create(office);
}
user.office_membership = officeData;
}
if (user.contact!.email !== userData.mailRattachement) {
updates++;
user.contact!.email = userData.mailRattachement;
}
if (user.contact!.cell_phone_number !== userData.numeroMobile) {
updates++;
user.contact!.cell_phone_number = userData.numeroMobile;
}
if (updates != 0) await this.userService.update(user.uid!, user);
await this.userService.updateCheckedAt(user.uid!);
}
public async updateOffice(officeId: string) {
const officeInfos = await this.officeService.getByUid(officeId);
console.log(officeInfos)
const office = Office.hydrate<Office>(officeInfos!);
const searchParams = new URLSearchParams({
key: this.variables.IDNOT_API_KEY,
});
const officeRawData = await fetch(
`${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/entities/${office.idNot}?` +
searchParams,
{
method: "GET",
},
);
if (officeRawData.status === 404) {
await this.officeService.updateCheckedAt(office.uid!);
//await this.officeService.delete(office.uid!);
return;
}
const officeData = (await officeRawData.json()) as IOfficeData;
console.log(officeData);
let updates = 0;
if(office.name !== officeData.denominationSociale) {
updates++;
office.name = officeData.denominationSociale;
}
if(office.office_status !== this.getOfficeStatus(officeData.statutEntite.name)) {
updates++;
office.office_status = this.getOfficeStatus(officeData.statutEntite.name);
}
if(updates != 0) await this.officeService.update(office.uid!, office);
await this.officeService.updateCheckedAt(office.uid!);
}
public async getOrCreateUser(decodedToken: IdNotJwtPayload) {
let user = await this.userService.getByProvider("idNot", decodedToken.sub);
if (!user) {
const searchParams = new URLSearchParams({
key: this.variables.IDNOT_API_KEY,
});
const userData = (await (
await fetch(`${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/rattachements/${decodedToken.profile_idn}?` + searchParams, {
method: "GET",
})
).json()) as IRattachementData;
if(!userData.statutDuRattachement || userData.entite.typeEntite.name !== "office") {
return null;
}
const officeLocationData = (await (
await fetch(`${this.variables.IDNOT_API_BASE_URL + userData.entite.locationsUrl}?` + searchParams, { method: "GET" })
).json()) as IOfficeLocation;
// if(officeLocationData.result[0]!.adrGeoCodePostal.slice(0,2) !== "35") {
// return null;
// }
const role = await this.getRole(userData.typeLien.name);
const userToAdd = {
idNot: decodedToken.sub,
office_membership: {
idNot: decodedToken.entity_idn,
name: userData.entite.denominationSociale,
crpcen: userData.entite.codeCrpcen,
office_status: this.getOfficeStatus(userData.entite.statutEntite.name),
address: {
address: officeLocationData.result[0]!.adrGeo4,
city: officeLocationData.result[0]!.adrGeoVille.split(" ")[0] ?? officeLocationData.result[0]!.adrGeoVille, //officeLocationData.result[0]!.adrPostaleVille,
zip_code: Number(officeLocationData.result[0]!.adrGeoCodePostal),
created_at: null,
updated_at: null,
},
created_at: null,
updated_at: null,
},
role: role,
contact: {
first_name: userData.personne.prenom,
last_name: userData.personne.nomUsuel,
email: userData.mailRattachement,
phone_number: userData.numeroTelephone,
cell_phone_number: userData.numeroMobile ?? userData.numeroTelephone,
civility: userData.personne.civilite,
created_at: null,
updated_at: null,
},
};
const userToHydrate = User.hydrate<User>(userToAdd);
return await this.userService.create(userToHydrate);
}
return user;
}
public async updateUsers() {
const usersReq = await this.userService.getUsersToBeChecked();
const users = User.hydrateArray<User>(usersReq);
users.forEach(async (user) => {
await this.updateUser(user.uid!);
});
}
public async updateOffices() {
const officesReq = await this.officeService.getOfficesToBeChecked();
const offices = Office.hydrateArray<Office>(officesReq);
offices.forEach(async (office) => {
await this.updateOffice(office.uid!);
});
}
}

View File

@ -0,0 +1,59 @@
import BaseService from "@Services/BaseService";
import { Service } from "typedi";
import { BackendVariables } from "@Common/config/variables/Variables";
@Service()
export default class SecureService extends BaseService {
constructor(protected variables: BackendVariables) {
super();
}
/**
* @description : anchor a sequence of hashes
* @throws {Error} If secure job cannot be created
*/
public async anchor(hash_sources: string[]) {
const url = new URL(this.variables.SECURE_API_BASE_URL.concat("/flows/v2/anchor"));
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
apiKey: this.variables.SECURE_API_KEY,
},
body: JSON.stringify({
hash_sources,
callback_url: "",
callback_config: {},
}),
});
return await response.json();
}
/**
* @description : verify if a sequence of hashes is anchored
* @throws {Error} If secure job cannot be found
*/
public async verify(hash_sources: string[]) {
const params = new URLSearchParams();
hash_sources.forEach((hash) => {
params.append("hash_sources", hash);
});
const url = new URL(this.variables.SECURE_API_BASE_URL.concat("/flows/v2/verify?").concat(params.toString()));
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
apiKey: this.variables.SECURE_API_KEY,
},
});
return await response.json();
}
}

View File

@ -34,6 +34,23 @@ export default class OfficesService extends BaseService {
return this.officeRepository.update(uid, officeEntity); return this.officeRepository.update(uid, officeEntity);
} }
/**
* @description : Modify an office check date
* @throws {Error} If user modification failed
* @deprecate
*/
public updateCheckedAt(uid: string): Promise<Offices> {
return this.officeRepository.updateCheckedAt(uid);
}
/**
* @description : delete an office
* @throws {Error} If office cannot be modified
*/
public async delete(uid: string): Promise<Offices> {
return this.officeRepository.delete(uid);
}
/** /**
* @description : Get a office by uid * @description : Get a office by uid
* @throws {Error} If office cannot be get * @throws {Error} If office cannot be get
@ -41,4 +58,11 @@ export default class OfficesService extends BaseService {
public async getByUid(uid: string, query?: any): Promise<Offices | null> { public async getByUid(uid: string, query?: any): Promise<Offices | null> {
return this.officeRepository.findOneByUid(uid, query); return this.officeRepository.findOneByUid(uid, query);
} }
/**
* @description : Get users to be checked with IdNot API
*/
public getOfficesToBeChecked() {
return this.officeRepository.findManyToCheck();
}
} }

View File

@ -35,6 +35,23 @@ export default class UsersService extends BaseService {
return this.userRepository.update(uid, userEntity); return this.userRepository.update(uid, userEntity);
} }
/**
* @description : Modify a user check date
* @throws {Error} If user modification failed
* @deprecate
*/
public updateCheckedAt(uid: string): Promise<Users> {
return this.userRepository.updateCheckedAt(uid);
}
/**
* @description : Delete a user
* @throws {Error} If user modification failed
*/
public delete(uid: string): Promise<Users> {
return this.userRepository.delete(uid);
}
/** /**
* @description : Get a user by uid * @description : Get a user by uid
* @throws {Error} If user cannot be get by uid * @throws {Error} If user cannot be get by uid
@ -66,4 +83,12 @@ export default class UsersService extends BaseService {
public getByProvider(providerName: string, id: string) { public getByProvider(providerName: string, id: string) {
return this.userRepository.findOneByProvider(providerName, id); return this.userRepository.findOneByProvider(providerName, id);
} }
/**
* @description : Get users to be checked with IdNot API
*/
public getUsersToBeChecked() {
return this.userRepository.findManyToCheck();
}
} }