Compare commits

...

10 Commits

14 changed files with 168 additions and 116 deletions

View File

@@ -18,9 +18,15 @@ class Registry:
self.current_firm = CurrentFirm.get_current(self.db) self.current_firm = CurrentFirm.get_current(self.db)
async def set_user(self, user): def check_user(self, user):
for firm in user.firms: for firm in user.firms:
if firm.instance == self.instance and firm.firm == self.firm: if firm.instance == self.instance and firm.firm == self.firm:
return True
raise PermissionError
async def set_user(self, user):
self.check_user(user)
partner = await Partner.get_by_user_id(self.db, user.id) partner = await Partner.get_by_user_id(self.db, user.id)
partner_entity = await Entity.get(self.db, partner.entity_id) partner_entity = await Entity.get(self.db, partner.entity_id)
self.user = user self.user = user
@@ -33,26 +39,32 @@ class Registry:
async def get_tenant_registry(instance: str, firm: str, db_client=Depends(get_db_client)) -> Registry: async def get_tenant_registry(instance: str, firm: str, db_client=Depends(get_db_client)) -> Registry:
registry = Registry(db_client, instance, firm) registry = Registry(db_client, instance, firm)
if await registry.current_firm is None: if await registry.current_firm is None:
raise HTTPException(status_code=405, detail=f"Firm needs to be initialized first") raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
return registry return registry
async def get_authed_tenant_registry(registry=Depends(get_tenant_registry), user=Depends(get_current_user)) -> Registry: async def get_authed_tenant_registry(instance: str, firm: str, db_client=Depends(get_db_client), user=Depends(get_current_user)) -> Registry:
registry = Registry(db_client, instance, firm)
try: try:
await registry.set_user(user) registry.check_user(user)
except PermissionError: except PermissionError:
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.") raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
if await registry.current_firm is None:
raise HTTPException(status_code=405, detail=f"Firm needs to be initialized first")
await registry.set_user(user)
return registry return registry
async def get_uninitialized_registry(instance: str, firm: str, db_client=Depends(get_db_client), user=Depends(get_current_user)) -> Registry: async def get_uninitialized_registry(instance: str, firm: str, db_client=Depends(get_db_client), user=Depends(get_current_user)) -> Registry:
registry = Registry(db_client, instance, firm) registry = Registry(db_client, instance, firm)
if await registry.current_firm is not None:
raise HTTPException(status_code=409, detail="Firm configuration already exists")
try: try:
await registry.set_user(user) registry.check_user(user)
except PermissionError: except PermissionError:
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.") raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
if await registry.current_firm is not None:
raise HTTPException(status_code=409, detail="Firm configuration already exists")
await registry.set_user(user)
return registry return registry

View File

@@ -67,7 +67,7 @@ class AuthenticationBackendMe(AuthenticationBackend):
class CookieTransportOauth(CookieTransport): class CookieTransportOauth(CookieTransport):
async def get_login_response(self, token: str) -> Response: async def get_login_response(self, token: str) -> Response:
response = RedirectResponse("/auth/login?oauth=success", status_code=status.HTTP_301_MOVED_PERMANENTLY) response = RedirectResponse("/login?oauth=success", status_code=status.HTTP_301_MOVED_PERMANENTLY)
return self._set_login_cookie(response, token) return self._set_login_cookie(response, token)
@staticmethod @staticmethod

View File

@@ -43,6 +43,7 @@
"mui-tiptap": "^1.18.1", "mui-tiptap": "^1.18.1",
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.30.0", "react-hook-form": "^7.30.0",
"react-i18next": "^15.5.1", "react-i18next": "^15.5.1",
"react-router": "^7.0.2" "react-router": "^7.0.2"
@@ -9180,6 +9181,18 @@
"react": "^18.3.1" "react": "^18.3.1"
} }
}, },
"node_modules/react-error-boundary": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz",
"integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"peerDependencies": {
"react": ">=16.13.1"
}
},
"node_modules/react-hook-form": { "node_modules/react-hook-form": {
"version": "7.56.1", "version": "7.56.1",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.1.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.1.tgz",

View File

@@ -39,6 +39,7 @@
"mui-tiptap": "^1.18.1", "mui-tiptap": "^1.18.1",
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.30.0", "react-hook-form": "^7.30.0",
"react-i18next": "^15.5.1", "react-i18next": "^15.5.1",
"react-router": "^7.0.2" "react-router": "^7.0.2"

View File

@@ -6,11 +6,7 @@ import { RefineSnackbarProvider, useNotificationProvider } from "@refinedev/mui"
import CssBaseline from "@mui/material/CssBaseline"; import CssBaseline from "@mui/material/CssBaseline";
import GlobalStyles from "@mui/material/GlobalStyles"; import GlobalStyles from "@mui/material/GlobalStyles";
import HistoryEduIcon from '@mui/icons-material/HistoryEdu'; import HistoryEduIcon from '@mui/icons-material/HistoryEdu';
import routerBindings, { import routerBindings, { DocumentTitleHandler, UnsavedChangesNotifier } from "@refinedev/react-router";
CatchAllNavigate,
DocumentTitleHandler,
UnsavedChangesNotifier,
} from "@refinedev/react-router";
import { BrowserRouter, Outlet, Route, Routes } from "react-router"; import { BrowserRouter, Outlet, Route, Routes } from "react-router";
import authProvider from "./providers/auth-provider"; import authProvider from "./providers/auth-provider";
import dataProvider from "./providers/data-provider"; import dataProvider from "./providers/data-provider";
@@ -61,7 +57,8 @@ function App() {
queries: { queries: {
retry: (failureCount, error) => { retry: (failureCount, error) => {
// @ts-ignore // @ts-ignore
if (error.statusCode >= 400 && error.statusCode <= 499) { const status = error.statusCode ? error.statusCode : error.status
if (status >= 400 && status<= 499) {
return false return false
} }
return failureCount < 4 return failureCount < 4
@@ -76,7 +73,7 @@ function App() {
<Routes> <Routes>
<Route <Route
element={( element={(
<Authenticated key="authenticated-routes" redirectOnFail="/login" fallback={<CatchAllNavigate to="/login"/>}> <Authenticated key="authenticated-routes" fallback={<Login />}>
<Outlet /> <Outlet />
</Authenticated> </Authenticated>
)} )}

View File

@@ -3,6 +3,8 @@ import { IFirm } from "../interfaces";
import { useParams } from "react-router"; import { useParams } from "react-router";
import { useOne } from "@refinedev/core"; import { useOne } from "@refinedev/core";
import { CircularProgress } from "@mui/material"; import { CircularProgress } from "@mui/material";
import { FirmInitForm } from "../pages/firm";
import { Header } from "../components";
type FirmContextType = { type FirmContextType = {
currentFirm: IFirm, currentFirm: IFirm,
@@ -19,19 +21,27 @@ export const FirmContextProvider: React.FC<PropsWithChildren> = ({ children }: P
const { data, isError, error, isLoading } = useOne({resource: 'firm', id: `${instance}/${firm}/`, errorNotification: false}); const { data, isError, error, isLoading } = useOne({resource: 'firm', id: `${instance}/${firm}/`, errorNotification: false});
if (instance === undefined || firm === undefined) { if (instance === undefined || firm === undefined) {
return "Error" throw({statusCode: 400});
} }
const currentFirm: IFirm = { instance, firm }
if (isLoading) { if (isLoading) {
return <CircularProgress /> return <CircularProgress />
} }
let value: FirmContextType = { if (isError && error) {
currentFirm: {instance, firm} if (error.statusCode == 405) {
return <><Header /><FirmInitForm currentFirm={currentFirm} /></>
} }
if (!isError || error?.statusCode != 405) { if (error.statusCode == 404) {
value.currentFirm.entity = data?.data.entity; throw error;
value.partnerMap = new Map(data?.data.partner_list.map((item: any) => [item.id, item.label])); }
}
currentFirm.entity = data?.data.entity;
let value: FirmContextType = {
currentFirm: currentFirm,
partnerMap: new Map(data?.data.partner_list.map((item: any) => [item.id, item.label])),
} }
return ( return (

View File

@@ -2,6 +2,7 @@ import { RJSFSchema } from '@rjsf/utils';
import i18n from '../../../i18n' import i18n from '../../../i18n'
import { JSONSchema7Definition } from "json-schema"; import { JSONSchema7Definition } from "json-schema";
import { GridColDef } from "@mui/x-data-grid"; import { GridColDef } from "@mui/x-data-grid";
import { GridColType } from "@mui/x-data-grid/models/colDef/gridColType";
const API_URL = "/api/v1"; const API_URL = "/api/v1";
@@ -120,20 +121,34 @@ function buildColumns (rawSchemas: RJSFSchema, resourceName: string, prefix: str
const subresourceName = get_reference_name(prop.items); const subresourceName = get_reference_name(prop.items);
result = result.concat(buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name)) result = result.concat(buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name))
} else { } else {
let column: GridColDef = { let valueGetter: undefined|((value: any, row: any) => any) = undefined;
field: prefix ? `${prefix}.${prop_name}` : prop_name, let type: GridColType = "string";
headerName: i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prop_name)}`, prop.title) as string, if (is_array(prop)) {
valueGetter: (value: any, row: any ) => { valueGetter = (value: any[], row: any ) => {
if (prefix === undefined) { return value.concat(".");
return value;
} }
} else if (prefix !== undefined) {
valueGetter = (value: any, row: any ) => {
let parent = row; let parent = row;
for (const column of prefix.split(".")) { for (const col of prefix.split(".")) {
parent = parent[column]; parent = parent[col];
} }
return parent ? parent[prop_name] : ""; return parent ? parent[prop_name] : "";
} }
} else {
if (prop.type == "string" && prop.format == "date-time") {
type = "dateTime"
valueGetter = (value: string) => new Date(value)
}
}
if (prop.type == "string" && prop.format == "date-time") {
}
const column: GridColDef = {
field: prefix ? `${prefix}.${prop_name}` : prop_name,
headerName: i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prop_name)}`, prop.title) as string,
type: type,
valueGetter: valueGetter
} }
result.push(column); result.push(column);
} }

View File

@@ -0,0 +1,4 @@
export const Error404Page = () => {
return <h2>EROR NO FUND</h2>
};

View File

@@ -24,7 +24,7 @@ const ListEntity = () => {
const columns = [ const columns = [
{ field: "entity_data.type", column: { width: 110 }}, { field: "entity_data.type", column: { width: 110 }},
{ field: "label", column: { flex: 1 }}, { field: "label", column: { flex: 1 }},
{ field: "updated_at", column: { flex: 1 }}, { field: "updated_at", column: { width: 160 }},
]; ];
return <List<Entity> resource={`entities`} schemaName={"Entity"} columns={columns} /> return <List<Entity> resource={`entities`} schemaName={"Entity"} columns={columns} />
} }

View File

@@ -38,6 +38,10 @@ const Edit = <T,>(props: EditProps) => {
return <Navigate to="../" /> return <Navigate to="../" />
} }
if (query.error?.status == 404) {
throw query.error
}
const record = query.data.data; const record = query.data.data;
return ( return (
<> <>

View File

@@ -54,7 +54,7 @@ const List = <T extends GridValidRowModel>(props: ListProps) => {
const { currentFirm } = useContext(FirmContext); const { currentFirm } = useContext(FirmContext);
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}` const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const { dataGridProps } = useDataGrid<T>({ const { dataGridProps, tableQueryResult } = useDataGrid<T>({
resource: `${resourceBasePath}/${resource}`, resource: `${resourceBasePath}/${resource}`,
}); });
const navigate = useNavigate(); const navigate = useNavigate();
@@ -85,6 +85,10 @@ const List = <T extends GridValidRowModel>(props: ListProps) => {
return <CircularProgress /> return <CircularProgress />
} }
if (tableQueryResult.error?.status == 404) {
throw tableQueryResult.error
}
return ( return (
<RefineList> <RefineList>
<Link to={"create"} > <Link to={"create"} >
@@ -94,7 +98,7 @@ const List = <T extends GridValidRowModel>(props: ListProps) => {
{...dataGridProps} {...dataGridProps}
columns={columnSchema.columns} columns={columnSchema.columns}
onRowClick={handleRowClick} onRowClick={handleRowClick}
pageSizeOptions={[10, 15, 20, 50, 100]} pageSizeOptions={[10, 15, 25, 50, 100]}
initialState={{ initialState={{
columns: { columns: {
columnVisibilityModel: columnSchema.columnVisibilityModel columnVisibilityModel: columnSchema.columnVisibilityModel

View File

@@ -1,7 +1,7 @@
import { Route, Routes, Link } from "react-router"; import { Route, Routes, Link } from "react-router";
import React, { useContext } from "react"; import React from "react";
import { useForm, useOne, useTranslation } from "@refinedev/core"; import { useForm, useTranslation } from "@refinedev/core";
import { FirmContext, FirmContextProvider } from "../../contexts/FirmContext"; import { FirmContextProvider } from "../../contexts/FirmContext";
import { Header } from "../../components"; import { Header } from "../../components";
import { CrudForm } from "../../lib/crud/components/crud-form"; import { CrudForm } from "../../lib/crud/components/crud-form";
import { IFirm } from "../../interfaces"; import { IFirm } from "../../interfaces";
@@ -10,11 +10,14 @@ import { ContractRoutes } from "./ContractRoutes";
import { DraftRoutes } from "./DraftRoutes"; import { DraftRoutes } from "./DraftRoutes";
import { TemplateRoutes } from "./TemplateRoutes"; import { TemplateRoutes } from "./TemplateRoutes";
import { ProvisionRoutes } from "./ProvisionRoutes"; import { ProvisionRoutes } from "./ProvisionRoutes";
import { ErrorBoundary } from "react-error-boundary";
import { Error404Page } from "../ErrorPage";
export const FirmRoutes = () => { export const FirmRoutes = () => {
return ( return (
<Routes> <Routes>
<Route path="/:instance/:firm/*" element={ <Route path="/:instance/:firm/*" element={
<ErrorBoundary fallback={<><Header /><Error404Page /></>} >
<FirmContextProvider> <FirmContextProvider>
<Header /> <Header />
<Routes> <Routes>
@@ -26,6 +29,7 @@ export const FirmRoutes = () => {
<Route path="/contracts/*" element={ <ContractRoutes /> } /> <Route path="/contracts/*" element={ <ContractRoutes /> } />
</Routes> </Routes>
</FirmContextProvider> </FirmContextProvider>
</ErrorBoundary>
} /> } />
</Routes> </Routes>
); );
@@ -51,7 +55,7 @@ type FirmInitFormPros = {
currentFirm: IFirm currentFirm: IFirm
} }
const FirmInitForm = (props: FirmInitFormPros) => { export const FirmInitForm = (props: FirmInitFormPros) => {
const { currentFirm } = props; const { currentFirm } = props;
const { translate: t } = useTranslation(); const { translate: t } = useTranslation();
const resourceBasePath = `firm` const resourceBasePath = `firm`

View File

@@ -12,7 +12,7 @@ const DEFAULT_LOGIN_REDIRECT = "/hub"
const authProvider: AuthProvider = { const authProvider: AuthProvider = {
login: async ({ providerName, email, password }) => { login: async ({ providerName, email, password }) => {
const to_param = findGetParameter("to"); const to_param = findGetParameter("to");
const redirect = to_param === null ? DEFAULT_LOGIN_REDIRECT : to_param const redirect = to_param === null ? getLoginRedirect() : to_param
if (providerName) { if (providerName) {
let scope = {}; let scope = {};
if (providerName === "google") { if (providerName === "google") {
@@ -61,18 +61,22 @@ const authProvider: AuthProvider = {
const response = await fetch(`${API_URL}/hub/auth/logout`, { method: "POST" }); const response = await fetch(`${API_URL}/hub/auth/logout`, { method: "POST" });
if (response.status == 204 || response.status == 401) { if (response.status == 204 || response.status == 401) {
forget_user(); forget_user();
return { return { success: true };
success: true,
redirectTo: "/",
};
} }
return { success: false }; return { success: false };
}, },
check: async () => { check: async () => {
if (get_user() == null) { const user = get_user();
if (user == null || isEmpty(user)) {
const user_data = await get_me();
if (user_data) {
store_user(user_data)
return { authenticated: true }
}
return { return {
authenticated: false, authenticated: false,
redirectTo: "/login",
logout: true logout: true
} }
} }
@@ -84,11 +88,7 @@ const authProvider: AuthProvider = {
return user; return user;
} }
const response = await fetch(`${API_URL}/hub/users/me`); const user_data = get_me()
if (response.status < 200 || response.status > 299) {
return null;
}
const user_data = await response.json();
store_user(user_data) store_user(user_data)
return user_data; return user_data;
@@ -154,9 +154,8 @@ const authProvider: AuthProvider = {
if (error?.status === 401) { if (error?.status === 401) {
forget_user(); forget_user();
return { return {
redirectTo: "/login",
logout: true,
error: { message: "Authentication required" }, error: { message: "Authentication required" },
logout: true,
} as OnErrorResponse; } as OnErrorResponse;
} }
else if (error?.status === 403) { else if (error?.status === 403) {
@@ -170,6 +169,15 @@ const authProvider: AuthProvider = {
}, },
}; };
async function get_me() {
const response = await fetch(`${API_URL}/hub/users/me`);
if (response.status < 200 || response.status > 299) {
return null;
}
const user_data = await response.json();
return user_data
}
function store_user(user: any) { function store_user(user: any) {
localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(user)); localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(user));
} }
@@ -200,4 +208,11 @@ function findGetParameter(parameterName: string) {
return result; return result;
} }
function getLoginRedirect() {
if (location.pathname == "/login") {
return DEFAULT_LOGIN_REDIRECT
}
return location.pathname + location.search;
}
export default authProvider; export default authProvider;

View File

@@ -2,6 +2,18 @@ import type { DataProvider, HttpError } from "@refinedev/core";
const API_URL = "/api/v1"; const API_URL = "/api/v1";
function handleErrors(response: { status: number, statusText: string }) {
let message = response.statusText
if (response.status == 405) {
message = "Resource is not ready";
}
const error: HttpError = {
message: message,
statusCode: response.status,
};
return Promise.reject(error);
}
const dataProvider: DataProvider = { const dataProvider: DataProvider = {
getOne: async ({ resource, id, meta }) => { getOne: async ({ resource, id, meta }) => {
if (id === "") { if (id === "") {
@@ -9,14 +21,7 @@ const dataProvider: DataProvider = {
} }
const response = await fetch(`${API_URL}/${resource}/${id}`); const response = await fetch(`${API_URL}/${resource}/${id}`);
if (response.status < 200 || response.status > 299) { if (response.status < 200 || response.status > 299) {
if (response.status == 405) { return handleErrors(response);
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();
@@ -35,17 +40,10 @@ const dataProvider: DataProvider = {
}); });
if (response.status < 200 || response.status > 299) { if (response.status < 200 || response.status > 299) {
if (response.status == 405) { return handleErrors(response);
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 };
}, },
getList: async ({ resource, pagination, filters, sorters, meta }) => { getList: async ({ resource, pagination, filters, sorters, meta }) => {
@@ -72,14 +70,7 @@ 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) { if (response.status < 200 || response.status > 299) {
if (response.status == 405) { return handleErrors(response);
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();
@@ -105,18 +96,10 @@ const dataProvider: DataProvider = {
}); });
if (response.status < 200 || response.status > 299) { if (response.status < 200 || response.status > 299) {
if (response.status == 405) { return handleErrors(response);
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 };
}, },
deleteOne: async ({ resource, id, variables, meta }) => { deleteOne: async ({ resource, id, variables, meta }) => {
@@ -125,21 +108,11 @@ const dataProvider: DataProvider = {
}); });
if (response.status < 200 || response.status > 299) { if (response.status < 200 || response.status > 299) {
if (response.status == 405) { return handleErrors(response);
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
};
}, },
getApiUrl: () => API_URL, getApiUrl: () => API_URL,
// Optional methods: // Optional methods: