Compare commits

...

4 Commits

Author SHA1 Message Date
75ff63a529 Cleaner header and hub 2025-04-12 01:16:44 +02:00
b04ee4cb92 UPdating oauth callbacks 2025-04-11 22:15:56 +02:00
4e613554e6 Reformating route and renaming firm and correcting login redirections 2025-04-11 22:00:57 +02:00
d1718becde Refactoring Hub Firms routes 2025-04-11 21:58:04 +02:00
12 changed files with 231 additions and 88 deletions

View File

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

View File

@@ -5,25 +5,29 @@ from pymongo import IndexModel
from hub.core import CrudDocument
class Firm(CrudDocument):
name: str = Field()
instance: str = Field()
firm: str = Field()
owner: PydanticObjectId = Field()
@classmethod
def get_by_name(cls, instance, firm):
return cls.find_one({"instance": instance, "firm": firm})
def compute_label(self) -> str:
return self.name
return f"{self.instance} / {self.firm}"
class Settings:
indexes = [
IndexModel(["name", "instance"], unique=True),
IndexModel(["instance", "firm"], unique=True),
]
class FirmRead(BaseModel):
instance: str = Field()
name: str = Field()
firm: str = Field()
class FirmCreate(FirmRead):
instance: str = Field(max_length=32, min_length=3, pattern="^[0-9a-z-]+$")
name: str = Field(max_length=32, min_length=3, pattern="^[0-9a-z-]+$")
firm: str = Field(max_length=32, min_length=3, pattern="^[0-9a-z-]+$")
class FirmUpdate(BaseModel):
owner: PydanticObjectId = Field()

View File

@@ -1,4 +1,3 @@
from beanie import PydanticObjectId
from fastapi import APIRouter, Depends, HTTPException
from hub.auth import get_current_user
@@ -7,42 +6,41 @@ from hub.firm import Firm, FirmRead, FirmCreate, FirmUpdate
router = APIRouter()
@router.get("/", response_model=list[FirmRead], response_description="{} records retrieved".format(Firm.__name__))
@router.get("/", response_model=list[FirmRead], response_description="List of firms owned by the current user")
async def read_list(user=Depends(get_current_user)) -> list[FirmRead]:
return await Firm.find({ "owner": user.id}).to_list()
@router.post("/", response_description="{} added to the database".format(Firm.__name__))
@router.post("/", response_description="Firm added to the database")
async def create(item: FirmCreate, user=Depends(get_current_user)) -> FirmRead:
firm_dict = {"name": item.name, "instance": item.instance}
exists = await Firm.find_one(firm_dict)
exists = await Firm.get_by_name(item.instance, item.firm)
if exists:
raise HTTPException(status_code=400, detail="Firm already exists")
record = Firm(created_by=user.id, updated_by=user.id, owner=user.id, **item.model_dump())
o = await record.create()
user.firms.append(firm_dict)
user.firms.append({"instance": item.instance, "firm": item.firm})
await user.save()
return FirmRead(**o.model_dump())
@router.get("/{id}", response_description="{} record retrieved".format(Firm.__name__))
async def read_id(id: PydanticObjectId, user=Depends(get_current_user)) -> FirmRead:
item = await Firm.get(id)
@router.get("/{instance}/{firm}", response_description="Firm retrieved")
async def read_id(instance: str, firm: str, user=Depends(get_current_user)) -> FirmRead:
item = await Firm.get_by_name(instance, firm)
if not item or not user.belong_to(item) not in user.firms:
raise HTTPException(status_code=404, detail="Item not found")
return FirmRead(**item.model_dump())
@router.put("/{id}", response_description="{} record updated".format(Firm.__name__))
async def update(id: PydanticObjectId, req: FirmUpdate, user=Depends(get_current_user)) -> FirmRead:
item = await Firm.get(id)
if not item:
raise HTTPException(
status_code=404,
detail="{} record not found!".format(Firm.__name__)
)
@router.put("/{instance}/{firm}", response_description="Firm updated")
async def update(instance: str, firm: str, req: FirmUpdate, user=Depends(get_current_user)) -> FirmRead:
item = await Firm.get_by_name(instance, firm)
if not item or not user.belong_to(item) not in user.firms:
raise HTTPException(status_code=404, detail="Item not found")
if item.owner != user.id:
raise HTTPException(
status_code=403,
detail="Insufficient credentials to modify {} record".format(Firm.__name__)
detail="Insufficient credentials to modify Firm"
)
req = {k: v for k, v in req.model_dump().items() if v is not None}
@@ -53,21 +51,19 @@ async def update(id: PydanticObjectId, req: FirmUpdate, user=Depends(get_current
await item.update(update_query)
return FirmRead(**item.dict())
@router.delete("/{id}", response_description="{} record deleted from the database".format(Firm.__name__))
async def delete(id: PydanticObjectId, user=Depends(get_current_user)) -> dict:
item = await Firm.get(id)
if not item:
raise HTTPException(
status_code=404,
detail="{} record not found!".format(Firm.__name__)
)
@router.delete("/{instance}/{firm}", response_description="Firm deleted from the database")
async def delete(instance: str, firm: str, user=Depends(get_current_user)) -> dict:
item = await Firm.get_by_name(instance, firm)
if not item or not user.belong_to(item) not in user.firms:
raise HTTPException(status_code=404, detail="Firm not found")
if item.owner != user.id:
raise HTTPException(
status_code=403,
detail="Insufficient credentials delete {} record".format(Firm.__name__)
detail="Insufficient credentials delete Firm"
)
await item.delete()
return {
"message": "{} deleted successfully".format(Firm.__name__)
"message": "Firm deleted successfully"
}

View File

@@ -8,5 +8,11 @@ from hub.firm import FirmRead
class UserSchema(BaseUser[PydanticObjectId]):
firms: list[FirmRead] = Field()
def belongs_to(self, firm):
for f in self.firms:
if f.instance == firm.instance and f.firm == firm.firm :
return True
return False
class UserUpdateSchema(BaseUserUpdate):
pass

View File

@@ -23,8 +23,8 @@ import { ForgotPassword } from "./components/auth/ForgotPassword";
import { UpdatePassword } from "./components/auth/UpdatePassword";
import { Header } from "./components";
import { Hub } from "./pages/hub";
import { CreateFirm } from "./pages/hub/CreateFirm";
import { HubRoutes } from "./pages/hub";
import { FirmRoutes } from "./pages/firm";
function App() {
return (
@@ -43,26 +43,42 @@ function App() {
syncWithLocation: true,
warnWhenUnsavedChanges: true,
useNewQueryKeys: true,
disableTelemetry: true
disableTelemetry: true,
reactQuery: {
clientConfig: {
defaultOptions: {
queries: {
retry: (failureCount, error) => {
// @ts-ignore
if (error.status >= 400 && error.status <= 499) {
return false
}
return failureCount < 4
},
}
}
}
}
}}
>
<Header />
<Routes>
<Route
element={(
<Authenticated key="authenticated-routes" redirectOnFail="/login" fallback={<CatchAllNavigate to="/login"/>}>
<Outlet />
</Authenticated>
)}
element={(
<Authenticated key="authenticated-routes" redirectOnFail="/auth/login" fallback={<CatchAllNavigate to="/auth/login"/>}>
<Outlet />
</Authenticated>
)}
>
<Route path="/hub" element={ <Hub /> } />
<Route path="/hub/create-firm" element={ <CreateFirm /> } />
<Route path="hub/*" element={<HubRoutes />} />
<Route path="firm/*" element={<FirmRoutes />} />
</Route>
<Route index element={<h1>HOME&nbsp;<Link to={"/login"}>Login</Link></h1>} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/update-password" element={<UpdatePassword />} />
<Route path="auth/*" element={<><Header /><Outlet /></>}>
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
<Route path="forgot-password" element={<ForgotPassword />} />
<Route path="update-password" element={<UpdatePassword />} />
</Route>
<Route index element={<><Header /><h1>HOME&nbsp;</h1></>} />
</Routes>
<UnsavedChangesNotifier />
<DocumentTitleHandler />

View File

@@ -1,7 +1,8 @@
import { Button } from "@mui/material";
import { useLogout } from "@refinedev/core";
export const Logout = () => {
const { mutate: logout } = useLogout();
return <button onClick={() => logout()} >Logout</button>;
return <Button onClick={() => logout()} >Logout</Button>;
};

View File

@@ -1,15 +1,19 @@
import DarkModeOutlined from "@mui/icons-material/DarkModeOutlined";
import LightModeOutlined from "@mui/icons-material/LightModeOutlined";
import HubIcon from '@mui/icons-material/Hub';
import { Button, Menu, MenuItem } from "@mui/material";
import AppBar from "@mui/material/AppBar";
import Avatar from "@mui/material/Avatar";
import IconButton from "@mui/material/IconButton";
import Stack from "@mui/material/Stack";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import React, { useContext } from "react";
import { Link } from "react-router";
import { useGetIdentity } from "@refinedev/core";
import { HamburgerMenu, RefineThemedLayoutV2HeaderProps } from "@refinedev/mui";
import React, { useContext } from "react";
import { ColorModeContext } from "../../contexts/color-mode";
import { FirmContext } from "../../contexts/FirmContext";
import { Logout } from "../auth/Logout";
import { IUser } from "../../interfaces";
@@ -17,34 +21,41 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
sticky = true,
}) => {
const { mode, setMode } = useContext(ColorModeContext);
const { currentFirm } = useContext(FirmContext);
const { data: user } = useGetIdentity<IUser>();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const openUserMenu = Boolean(anchorEl);
const handleOpenUserMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
}
const handleCloseUserMenu = () => {
setAnchorEl(null);
};
return (
<AppBar position={sticky ? "sticky" : "relative"}>
<Toolbar>
<Stack
direction="row"
width="100%"
justifyContent="flex-end"
justifyContent="space-between"
alignItems="center"
>
<HamburgerMenu />
{currentFirm && (
<Link to={`/firm/${currentFirm.instance}/${currentFirm.firm}`} ><Typography variant="h4" >{currentFirm.instance}&nbsp;/&nbsp;{currentFirm.firm}</Typography></Link>
)}
{!currentFirm && (
<Link to="/" ><Typography variant="h4">Roleplay&nbsp;Contracts</Typography></Link>
)}
<Link to="/hub"><HubIcon /></Link>
<Stack
direction="row"
width="100%"
justifyContent="flex-end"
alignItems="center"
>
<IconButton
color="inherit"
onClick={() => {
setMode();
}}
>
{mode === "dark" ? <LightModeOutlined /> : <DarkModeOutlined />}
</IconButton>
{(user?.email) && (
<Stack
direction="row"
@@ -52,7 +63,12 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
alignItems="center"
justifyContent="center"
>
{user?.email && (
<Button
id="user-menu-button"
aria-controls={openUserMenu ? 'user-menu' : undefined}
aria-haspopup="true"
aria-expanded={openUserMenu ? 'true' : undefined}
onClick={handleOpenUserMenu}>
<Typography
sx={{
display: {
@@ -63,12 +79,36 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
variant="subtitle2"
>
{user?.email}
</Typography>
)}
<Avatar src={"user?.avatar"} alt={user?.email} />
<Logout />
</Typography>&nbsp;
<Avatar src={"user?.avatar"} alt={user?.email} />
</Button>
<Menu
id="user-menu"
anchorEl={anchorEl}
open={openUserMenu}
onClose={handleCloseUserMenu}
MenuListProps={{
'aria-labelledby': 'user-menu-button',
}}
>
<MenuItem onClick={handleCloseUserMenu}><Logout /></MenuItem>
<MenuItem onClick={handleCloseUserMenu}>
<IconButton
color="inherit"
onClick={() => {
setMode();
}}
>
{mode === "dark" ? <LightModeOutlined /> : <DarkModeOutlined />}
</IconButton>
</MenuItem>
</Menu>
</Stack>
)}
{!user && (
<Link to="/auth/login"><Button>Login</Button></Link>
)}
</Stack>
</Stack>
</Toolbar>

View File

@@ -0,0 +1,25 @@
import React, { createContext, PropsWithChildren } from 'react';
import { IFirm } from "../interfaces";
import { useParams } from "react-router";
type FirmContextType = {
currentFirm: IFirm,
}
export const FirmContext = createContext<FirmContextType>(
{} as FirmContextType
);
export const FirmContextProvider: React.FC<PropsWithChildren> = ({ children }: PropsWithChildren) => {
const { instance, firm } = useParams<IFirm>()
if (instance === undefined || firm === undefined) {
return "Error"
}
return (
<FirmContext.Provider value={{currentFirm: {instance, firm}}} >
{ children }
</FirmContext.Provider>
);
}

View File

@@ -1,7 +1,7 @@
export type IFirm = {
instance: string,
name: string
firm: string
}
type User = {

View File

@@ -0,0 +1,28 @@
import {Route, Routes} from "react-router";
import React, { useContext } from "react";
import { FirmContext, FirmContextProvider } from "../../contexts/FirmContext";
import { Header } from "../../components";
export const FirmRoutes = () => {
return (
<>
<Routes>
<Route path="/:instance/:firm/*" element={
<FirmContextProvider>
<Header />
<Routes>
<Route index element={ <FirmHome /> } />
</Routes>
</FirmContextProvider>
} />
</Routes>
</>
);
}
const FirmHome = () => {
const { currentFirm } = useContext(FirmContext);
return (
<h1>This is la firme {currentFirm.instance} / {currentFirm.firm}</h1>
);
}

View File

@@ -1,38 +1,53 @@
import { Button } from "@mui/material";
import { Link } from "react-router";
import ExitToAppIcon from '@mui/icons-material/ExitToApp';
import React from 'react';
import {Link, Route, Routes} from "react-router";
import { useGetIdentity, useList } from "@refinedev/core";
import { IAuthUser, IFirm } from "../../interfaces";
import {CreateFirm} from "./CreateFirm";
import {Header} from "../../components";
export const Hub = () => {
export const HubRoutes = () => {
return (
<>
<Header />
<Routes>
<Route index element={ <HubHome /> } />
<Route path="create-firm" element={ <CreateFirm /> } />
</Routes>
</>
);
};
const HubHome = () => {
const { data: user } = useGetIdentity<IAuthUser>();
const { data: list } = useList<IFirm>({resource: "hub/users/firms/", pagination: { mode: "off" }}, )
if (user === undefined || list === undefined) {
return <p>Loading</p>
return <p>Loading</p>;
}
console.log("list data: ", list);
const ownedFirms = list.data;
if (user === undefined || ownedFirms === undefined) {
return <p>Loading</p>
}
console.log("owned firms: ", ownedFirms);
return (
<div>
<h1>HUB</h1>
<p>List of managed firms</p>
<ul>
{ownedFirms.map((f: IFirm, index) => (
<li key={index}>{f.instance} / {f.name}</li>
<li key={index}>{f.instance} / {f.firm}</li>
))}
</ul>
<Link to="/hub/create-firm" ><Button>Create a new firm</Button></Link>
<p>List of firm you're working at</p>
<ul>
{user.firms.map((f: IFirm, index) => (
<li key={index}>{f.instance} / {f.name}</li>
<li key={index}>
{f.instance} / {f.firm}&nbsp;<Link to={`/firm/${f.instance}/${f.firm}`}><ExitToAppIcon /></Link>
</li>
))}
</ul>
<Link to="/hub/create-firm" ><Button >Create a new firm</Button></Link>
</div>
);
};
)
}

View File

@@ -7,9 +7,12 @@ const LOCAL_STORAGE_USER_KEY = "rpk-gui-current-user";
const GOOGLE_SCOPES = { "scopes": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email" };
const DISCORD_SCOPES = { "scopes": "identify email" }
const DEFAULT_LOGIN_REDIRECT = "/hub"
export const authProvider: AuthProvider = {
login: async ({ providerName, email, password }) => {
const to_param = findGetParameter("to");
const redirect = to_param === null ? DEFAULT_LOGIN_REDIRECT : to_param
if (providerName) {
let scope = {};
if (providerName === "google") {
@@ -22,11 +25,14 @@ export const authProvider: AuthProvider = {
const response = await fetch(url, { method: "GET", },);
const body = await response.json();
if (to_param) {
localStorage.setItem("redirect_after_login", to_param);
}
localStorage.setItem("redirect_after_login", redirect);
window.location.href = body.authorization_url;
return { success: true };
return {
success: true,
redirectTo: ""
};
} else if (email !== undefined && password !== undefined) {
const params = new URLSearchParams({"grant_type": "password", "username": email, "password": password});
const response = await fetch(
@@ -42,7 +48,10 @@ export const authProvider: AuthProvider = {
const user = await response.json();
store_user(user);
return { success: true };
return {
success: true,
redirectTo: redirect,
};
}
}
@@ -52,7 +61,10 @@ export const authProvider: AuthProvider = {
const response = await fetch(`${API_URL}/hub/auth/logout`, { method: "POST" });
if (response.status == 204 || response.status == 401) {
forget_user();
return { success: true };
return {
success: true,
redirectTo: "/",
};
}
return { success: false };
},
@@ -60,7 +72,7 @@ export const authProvider: AuthProvider = {
if (get_user() == null) {
return {
authenticated: false,
redirectTo: "/login",
redirectTo: "/auth/login",
logout: true
}
}
@@ -142,7 +154,7 @@ export const authProvider: AuthProvider = {
if (error?.status === 401) {
forget_user();
return {
redirectTo: "/login",
redirectTo: "/auth/login",
logout: true,
error: { message: "Authentication required" },
} as OnErrorResponse;