Adding multi tenant check and Starting firm initialization
This commit is contained in:
@@ -3,10 +3,12 @@ from fastapi import APIRouter
|
|||||||
from firm.entity import entity_router
|
from firm.entity import entity_router
|
||||||
from firm.template import template_router
|
from firm.template import template_router
|
||||||
from firm.contract import contract_router
|
from firm.contract import contract_router
|
||||||
|
from firm.current_firm.routes import current_firm_router
|
||||||
|
|
||||||
|
|
||||||
firm_router = APIRouter(prefix="/{instance}/{firm}")
|
firm_router = APIRouter(prefix="/{instance}/{firm}")
|
||||||
|
|
||||||
|
firm_router.include_router(current_firm_router, tags=["Current Firm"])
|
||||||
firm_router.include_router(entity_router, prefix="/entities", tags=["Entity"], )
|
firm_router.include_router(entity_router, prefix="/entities", tags=["Entity"], )
|
||||||
firm_router.include_router(template_router, prefix="/templates", tags=["Template"], )
|
firm_router.include_router(template_router, prefix="/templates", tags=["Template"], )
|
||||||
firm_router.include_router(contract_router, prefix="/contracts", )
|
firm_router.include_router(contract_router, prefix="/contracts", )
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from weasyprint.text.fonts import FontConfiguration
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from firm.core.routes import get_tenant_db_cursor
|
from firm.core.depends import get_tenant_db_cursor
|
||||||
from firm.entity.models import Entity
|
from firm.entity.models import Entity
|
||||||
from firm.template.models import ProvisionTemplate
|
from firm.template.models import ProvisionTemplate
|
||||||
from firm.contract.models import ContractDraft, Contract, ContractStatus, replace_variables_in_value
|
from firm.contract.models import ContractDraft, Contract, ContractStatus, replace_variables_in_value
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from fastapi import Depends, HTTPException
|
from fastapi import Depends, HTTPException
|
||||||
|
|
||||||
from firm.core.routes import get_crud_router, get_logged_tenant_db_cursor
|
from firm.core.routes import get_crud_router
|
||||||
|
from firm.core.depends import get_logged_tenant_db_cursor
|
||||||
|
|
||||||
from firm.contract.models import Contract, ContractDraft, ContractDraftStatus, replace_variables_in_value, ContractFilters
|
from firm.contract.models import Contract, ContractDraft, ContractDraftStatus, replace_variables_in_value, ContractFilters
|
||||||
from firm.contract.schemas import ContractCreate, ContractRead, ContractUpdate, ContractInit
|
from firm.contract.schemas import ContractCreate, ContractRead, ContractUpdate, ContractInit
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from beanie import PydanticObjectId
|
from beanie import PydanticObjectId
|
||||||
from fastapi import HTTPException, Depends
|
from fastapi import HTTPException, Depends
|
||||||
|
|
||||||
from firm.core.routes import get_crud_router, get_logged_tenant_db_cursor
|
from firm.core.routes import get_crud_router
|
||||||
|
from firm.core.depends import get_logged_tenant_db_cursor
|
||||||
|
|
||||||
from firm.contract.models import ContractDraft, ContractDraftStatus, ContractDraftFilters
|
from firm.contract.models import ContractDraft, ContractDraftStatus, ContractDraftFilters
|
||||||
from firm.contract.schemas import ContractDraftCreate, ContractDraftRead, ContractDraftUpdate
|
from firm.contract.schemas import ContractDraftCreate, ContractDraftRead, ContractDraftUpdate
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import shutil
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from firm.contract.models import Contract, Party
|
from firm.contract.models import Contract, Party
|
||||||
from firm.core.routes import get_tenant_db_cursor
|
from firm.core.depends import get_tenant_db_cursor
|
||||||
|
|
||||||
|
|
||||||
signature_router = APIRouter()
|
signature_router = APIRouter()
|
||||||
|
|||||||
34
api/rpk-api/firm/core/depends.py
Normal file
34
api/rpk-api/firm/core/depends.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from fastapi import HTTPException, Depends
|
||||||
|
from hub.auth import get_current_user
|
||||||
|
|
||||||
|
from firm.db import get_db_client
|
||||||
|
from firm.current_firm import CurrentFirmModel
|
||||||
|
|
||||||
|
async def get_tenant_db_cursor(instance: str, firm: str, db_client=Depends(get_db_client)):
|
||||||
|
db_cursor = db_client[f"tenant_{instance}_{firm}"]
|
||||||
|
current_firm = await CurrentFirmModel.get(db_cursor)
|
||||||
|
if current_firm is None:
|
||||||
|
raise HTTPException(status_code=405, detail=f"Firm needs to be instantiated first")
|
||||||
|
db_cursor.firm = current_firm
|
||||||
|
return db_cursor
|
||||||
|
|
||||||
|
def get_logged_tenant_db_cursor(db_cursor=Depends(get_tenant_db_cursor), user=Depends(get_current_user)):
|
||||||
|
for firm in user.firms:
|
||||||
|
if firm == db_cursor.firm:
|
||||||
|
db_cursor.user = user
|
||||||
|
return db_cursor
|
||||||
|
|
||||||
|
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
|
||||||
|
|
||||||
|
async def get_uninitialized_tenant_db_cursor(instance: str, firm: str, db_client=Depends(get_db_client), user=Depends(get_current_user)):
|
||||||
|
db_cursor = db_client[f"tenant_{instance}_{firm}"]
|
||||||
|
current_firm = await CurrentFirmModel.get(db_cursor)
|
||||||
|
if current_firm is not None:
|
||||||
|
HTTPException(status_code=409, detail="Firm configuration already exists")
|
||||||
|
|
||||||
|
for firm in user.firms:
|
||||||
|
if firm == db_cursor.firm:
|
||||||
|
db_cursor.user = user
|
||||||
|
return db_cursor
|
||||||
|
|
||||||
|
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
|
||||||
@@ -9,9 +9,9 @@ from pydantic import BaseModel, Field, computed_field
|
|||||||
class CrudDocument(BaseModel):
|
class CrudDocument(BaseModel):
|
||||||
id: Optional[PydanticObjectId] = Field(default=None)
|
id: Optional[PydanticObjectId] = Field(default=None)
|
||||||
created_at: datetime = Field(default=datetime.now(UTC), nullable=False, title="Créé le")
|
created_at: datetime = Field(default=datetime.now(UTC), nullable=False, title="Créé le")
|
||||||
# created_by: str
|
created_by: str = Field(nullable=False, title="Créé par")
|
||||||
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False, title="Modifié le")
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), nullable=False, title="Modifié le")
|
||||||
# updated_by: str
|
updated_by: str = Field(nullable=False, title="Modifié par")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _id(self):
|
def _id(self):
|
||||||
@@ -37,8 +37,9 @@ class CrudDocument(BaseModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create(cls, db, create_schema):
|
async def create(cls, db, create_schema):
|
||||||
values = cls.model_validate(create_schema.model_dump()).model_dump(mode="json")
|
model_dict = create_schema.model_dump() | {"created_by": db.user, "updated_by":db.user}
|
||||||
result = await cls._get_collection(db).insert_one(values)
|
document = cls.model_validate(model_dict).model_dump(mode="json")
|
||||||
|
result = await cls._get_collection(db).insert_one(document)
|
||||||
|
|
||||||
return await cls.get(db, result.inserted_id)
|
return await cls.get(db, result.inserted_id)
|
||||||
|
|
||||||
@@ -56,12 +57,12 @@ class CrudDocument(BaseModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get(cls, db, model_id):
|
async def get(cls, db, model_id):
|
||||||
value = await cls._get_collection(db).find_one({"_id": model_id})
|
document = await cls._get_collection(db).find_one({"_id": model_id})
|
||||||
if not value:
|
if not document:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
value["id"] = value.pop("_id")
|
document["id"] = document.pop("_id")
|
||||||
return cls.model_validate(value)
|
return cls.model_validate(document)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def update(cls, db, model, update_schema):
|
async def update(cls, db, model, update_schema):
|
||||||
|
|||||||
@@ -5,26 +5,9 @@ from fastapi_filter import FilterDepends
|
|||||||
from fastapi_pagination import Page, add_pagination
|
from fastapi_pagination import Page, add_pagination
|
||||||
from fastapi_pagination.ext.motor import paginate
|
from fastapi_pagination.ext.motor import paginate
|
||||||
|
|
||||||
from hub.auth import get_current_user
|
from firm.core.depends import get_logged_tenant_db_cursor
|
||||||
from firm.core.models import CrudDocument
|
from firm.core.models import CrudDocument
|
||||||
from firm.core.schemas import Writer, Reader
|
from firm.core.schemas import Writer, Reader
|
||||||
from firm.db import get_db_client
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#instance: str="westside", firm: str="cht",
|
|
||||||
def get_tenant_db_cursor(db_client=Depends(get_db_client)):
|
|
||||||
instance = "westside"
|
|
||||||
firm = "cht"
|
|
||||||
return db_client[f"tenant_{instance}_{firm}"]
|
|
||||||
|
|
||||||
#instance: str="westside", firm: str="cht",
|
|
||||||
def get_logged_tenant_db_cursor(db_client=Depends(get_db_client), user=Depends(get_current_user)):
|
|
||||||
instance = "westside"
|
|
||||||
firm = "cht"
|
|
||||||
db_cursor = db_client[f"tenant_{instance}_{firm}"]
|
|
||||||
db_cursor.user = user
|
|
||||||
return db_cursor
|
|
||||||
|
|
||||||
def get_crud_router(model: CrudDocument, model_create: Writer, model_read: Reader, model_update: Writer, model_filter):
|
def get_crud_router(model: CrudDocument, model_create: Writer, model_read: Reader, model_update: Writer, model_filter):
|
||||||
model_name = model.__name__
|
model_name = model.__name__
|
||||||
|
|||||||
45
api/rpk-api/firm/current_firm/__init__.py
Normal file
45
api/rpk-api/firm/current_firm/__init__.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from firm.core.models import CrudDocument
|
||||||
|
from firm.core.schemas import Writer, Reader
|
||||||
|
from firm.entity.schemas import EntityIndividualCreate, EntityCorporationCreate
|
||||||
|
|
||||||
|
|
||||||
|
class CurrentFirmModel(CrudDocument):
|
||||||
|
instance: str
|
||||||
|
firm: str
|
||||||
|
name: str = Field(nullable=False)
|
||||||
|
|
||||||
|
# primary_color: str = Field()
|
||||||
|
# secondary_color: str = Field()
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
if isinstance(other, dict):
|
||||||
|
return self.instance == other["instance"] and self.firm == other["firm"]
|
||||||
|
return super().__eq__(other)
|
||||||
|
|
||||||
|
def compute_label(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get(cls, db):
|
||||||
|
document = await cls._get_collection(db).find_one({})
|
||||||
|
if not document:
|
||||||
|
return None
|
||||||
|
|
||||||
|
document["id"] = document.pop("_id")
|
||||||
|
return cls.model_validate(document)
|
||||||
|
|
||||||
|
|
||||||
|
class CurrentFirmSchemaRead(Reader):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CurrentFirmSchemaCreate(Writer):
|
||||||
|
corporation: EntityCorporationCreate = Field(title="Informations sur la firme")
|
||||||
|
owner: EntityIndividualCreate = Field(title="Informations sur le dirigeant")
|
||||||
|
|
||||||
|
|
||||||
|
class CurrentFirmSchemaUpdate(Writer):
|
||||||
|
pass
|
||||||
22
api/rpk-api/firm/current_firm/routes.py
Normal file
22
api/rpk-api/firm/current_firm/routes.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from firm.core.depends import get_logged_tenant_db_cursor, get_uninitialized_tenant_db_cursor
|
||||||
|
from firm.current_firm import CurrentFirmModel, CurrentFirmSchemaRead, CurrentFirmSchemaCreate, CurrentFirmSchemaUpdate
|
||||||
|
|
||||||
|
current_firm_router = APIRouter()
|
||||||
|
|
||||||
|
@current_firm_router.get("/", response_model=CurrentFirmSchemaRead, response_description=f"Current Firm records retrieved")
|
||||||
|
async def read(db=Depends(get_logged_tenant_db_cursor)) -> CurrentFirmSchemaRead:
|
||||||
|
return CurrentFirmSchemaRead.from_model(**CurrentFirmModel.get(db))
|
||||||
|
|
||||||
|
@current_firm_router.post("/", response_description=f"Current Firm added to the database")
|
||||||
|
async def create(schema: CurrentFirmSchemaCreate, db=Depends(get_uninitialized_tenant_db_cursor)) -> CurrentFirmSchemaRead:
|
||||||
|
await schema.validate_foreign_key(db)
|
||||||
|
record = await CurrentFirmModel.create(db, schema)
|
||||||
|
return CurrentFirmSchemaRead.from_model(record)
|
||||||
|
|
||||||
|
@current_firm_router.put("/", response_description=f"Current Firm record updated")
|
||||||
|
async def update(schema: CurrentFirmSchemaUpdate, db=Depends(get_logged_tenant_db_cursor)) -> CurrentFirmSchemaRead:
|
||||||
|
record = await CurrentFirmModel.get(db)
|
||||||
|
record = await CurrentFirmModel.update(db, record, schema)
|
||||||
|
return CurrentFirmSchemaRead.from_model(record)
|
||||||
@@ -13,5 +13,11 @@ class EntityCreate(Writer):
|
|||||||
class Config:
|
class Config:
|
||||||
title = "Création d'un client"
|
title = "Création d'un client"
|
||||||
|
|
||||||
|
class EntityIndividualCreate(EntityCreate):
|
||||||
|
entity_data: Individual
|
||||||
|
|
||||||
|
class EntityCorporationCreate(EntityCreate):
|
||||||
|
entity_data: Corporation
|
||||||
|
|
||||||
class EntityUpdate(EntityCreate):
|
class EntityUpdate(EntityCreate):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function App() {
|
|||||||
queries: {
|
queries: {
|
||||||
retry: (failureCount, error) => {
|
retry: (failureCount, error) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (error.status >= 400 && error.status <= 499) {
|
if (error.statusCode >= 400 && error.statusCode <= 499) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return failureCount < 4
|
return failureCount < 4
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import {Route, Routes} from "react-router";
|
|||||||
import React, { useContext } from "react";
|
import React, { useContext } from "react";
|
||||||
import { FirmContext, FirmContextProvider } from "../../contexts/FirmContext";
|
import { FirmContext, FirmContextProvider } from "../../contexts/FirmContext";
|
||||||
import { Header } from "../../components";
|
import { Header } from "../../components";
|
||||||
|
import { useOne } from "@refinedev/core";
|
||||||
|
import { CrudForm } from "../../lib/crud/components/crud-form";
|
||||||
|
import { IFirm } from "../../interfaces";
|
||||||
|
|
||||||
export const FirmRoutes = () => {
|
export const FirmRoutes = () => {
|
||||||
return (
|
return (
|
||||||
@@ -22,7 +25,31 @@ export const FirmRoutes = () => {
|
|||||||
|
|
||||||
const FirmHome = () => {
|
const FirmHome = () => {
|
||||||
const { currentFirm } = useContext(FirmContext);
|
const { currentFirm } = useContext(FirmContext);
|
||||||
|
const { data: firm, isError, error, isLoading } = useOne({resource: 'firm', id: `${currentFirm.instance}/${currentFirm.firm}`, errorNotification: false})
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <h1>Loading...</h1>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError && error?.statusCode == 405) {
|
||||||
|
return <FirmInitForm currentFirm={currentFirm} />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<h1>This is la firme {currentFirm.instance} / {currentFirm.firm}</h1>
|
<h1>This is la firme {currentFirm.instance} / {currentFirm.firm}</h1>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FirmInitFormPros = {
|
||||||
|
currentFirm: IFirm
|
||||||
|
}
|
||||||
|
|
||||||
|
const FirmInitForm = (props: FirmInitFormPros) => {
|
||||||
|
const { currentFirm } = props;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Initialization of {`${currentFirm.instance} / ${currentFirm.firm}`}</h1>
|
||||||
|
<CrudForm schemaName={"CurrentFirmSchemaCreate"} resource={`/firm/${currentFirm.instance}/${currentFirm.firm}`} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
import type { DataProvider } from "@refinedev/core";
|
import type { DataProvider, HttpError } from "@refinedev/core";
|
||||||
|
|
||||||
const API_URL = "/api/v1";
|
const API_URL = "/api/v1";
|
||||||
|
|
||||||
export const dataProvider: DataProvider = {
|
export const dataProvider: DataProvider = {
|
||||||
getOne: async ({ resource, id, meta }) => {
|
getOne: async ({ resource, id, meta }) => {
|
||||||
const response = id !== "" ? await fetch(`${API_URL}/${resource}/${id}`) : await fetch(`${API_URL}/${resource}`);
|
const response = id !== "" ? await fetch(`${API_URL}/${resource}/${id}`) : await fetch(`${API_URL}/${resource}`);
|
||||||
if (response.status < 200 || response.status > 299) throw response;
|
if (response.status < 200 || response.status > 299) {
|
||||||
|
if (response.status == 405) {
|
||||||
|
const error: HttpError = {
|
||||||
|
message: "Resource is not ready",
|
||||||
|
statusCode: 405,
|
||||||
|
};
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
throw response;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
@@ -22,8 +31,16 @@ export const dataProvider: DataProvider = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status < 200 || response.status > 299) throw response;
|
if (response.status < 200 || response.status > 299) {
|
||||||
|
if (response.status == 405) {
|
||||||
|
const error: HttpError = {
|
||||||
|
message: "Resource is not ready",
|
||||||
|
statusCode: 405,
|
||||||
|
};
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
throw response;
|
||||||
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
return { data };
|
return { data };
|
||||||
@@ -51,7 +68,16 @@ export const dataProvider: DataProvider = {
|
|||||||
|
|
||||||
const response = await fetch(`${API_URL}/${resource}?${params.toString()}`);
|
const response = await fetch(`${API_URL}/${resource}?${params.toString()}`);
|
||||||
|
|
||||||
if (response.status < 200 || response.status > 299) throw response;
|
if (response.status < 200 || response.status > 299) {
|
||||||
|
if (response.status == 405) {
|
||||||
|
const error: HttpError = {
|
||||||
|
message: "Resource is not ready",
|
||||||
|
statusCode: 405,
|
||||||
|
};
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
throw response;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
@@ -75,7 +101,16 @@ export const dataProvider: DataProvider = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status < 200 || response.status > 299) throw response;
|
if (response.status < 200 || response.status > 299) {
|
||||||
|
if (response.status == 405) {
|
||||||
|
const error: HttpError = {
|
||||||
|
message: "Resource is not ready",
|
||||||
|
statusCode: 405,
|
||||||
|
};
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
throw response;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
@@ -86,7 +121,16 @@ export const dataProvider: DataProvider = {
|
|||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status < 200 || response.status > 299) throw response;
|
if (response.status < 200 || response.status > 299) {
|
||||||
|
if (response.status == 405) {
|
||||||
|
const error: HttpError = {
|
||||||
|
message: "Resource is not ready",
|
||||||
|
statusCode: 405,
|
||||||
|
};
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
throw response;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user