From f1fe81a146958c817dd0abf4960f4c1bce15b080 Mon Sep 17 00:00:00 2001 From: ewandor Date: Thu, 10 Apr 2025 01:31:37 +0200 Subject: [PATCH] Improving user management and auto-refreshing user firms --- api/rpk-api/hub/auth/__init__.py | 15 +++--- api/rpk-api/hub/db.py | 2 +- api/rpk-api/hub/firm/routes.py | 49 +++++++++---------- api/rpk-api/hub/user/__init__.py | 12 +++-- api/rpk-api/hub/user/schemas.py | 9 +++- gui/rpk-gui/src/App.tsx | 4 +- gui/rpk-gui/src/components/auth/Logout.tsx | 6 +-- gui/rpk-gui/src/components/header/index.tsx | 13 ++--- gui/rpk-gui/src/interfaces/index.tsx | 13 +++++ .../src/lib/crud/components/crud-form.tsx | 12 +++-- gui/rpk-gui/src/pages/hub/CreateFirm.tsx | 14 +++++- gui/rpk-gui/src/pages/hub/index.tsx | 28 ++++++++++- gui/rpk-gui/src/providers/auth-provider.tsx | 21 ++++++-- 13 files changed, 131 insertions(+), 67 deletions(-) create mode 100644 gui/rpk-gui/src/interfaces/index.tsx diff --git a/api/rpk-api/hub/auth/__init__.py b/api/rpk-api/hub/auth/__init__.py index 5b86dd2..3bd62e3 100644 --- a/api/rpk-api/hub/auth/__init__.py +++ b/api/rpk-api/hub/auth/__init__.py @@ -1,19 +1,19 @@ import os -from typing import Any, Optional +from typing import Any -from beanie import PydanticObjectId +from beanie import PydanticObjectId, Document from fastapi import Depends, Response, status from fastapi_users import BaseUserManager, FastAPIUsers, schemas, models -from fastapi_users.authentication import AuthenticationBackend, BearerTransport, CookieTransport, Strategy +from fastapi_users.authentication import AuthenticationBackend, CookieTransport, Strategy from fastapi_users.authentication.strategy import AccessTokenDatabase, DatabaseStrategy -from fastapi_users_db_beanie.access_token import BeanieBaseAccessTokenDocument, BeanieAccessTokenDatabase +from fastapi_users_db_beanie.access_token import BeanieBaseAccessToken, BeanieAccessTokenDatabase from fastapi_users.openapi import OpenAPIResponseType from httpx_oauth.clients.google import GoogleOAuth2 from httpx_oauth.clients.discord import DiscordOAuth2 from starlette.responses import JSONResponse, RedirectResponse from hub.user import User, get_user_db -from hub.user.schemas import UserSchema +from hub.user.schemas import UserSchema, UserUpdateSchema SECRET = os.getenv("FASTAPI_USERS_SECRET") @@ -23,7 +23,7 @@ discord_oauth_client = DiscordOAuth2(os.getenv("DISCORD_CLIENT_ID"), os.getenv(" TOKEN_LIFETIME = 3600 -class AccessToken(BeanieBaseAccessTokenDocument): +class AccessToken(BeanieBaseAccessToken, Document): pass async def get_access_token_db(): @@ -84,10 +84,11 @@ auth_router = fastapi_users.get_auth_router(auth_backend, requires_verification= register_router = fastapi_users.get_register_router(UserSchema, schemas.BaseUserCreate) password_router = fastapi_users.get_reset_password_router() verification_router = fastapi_users.get_verify_router(UserSchema) -users_router = fastapi_users.get_users_router(UserSchema, schemas.BaseUserUpdate) +users_router = fastapi_users.get_users_router(UserSchema, UserUpdateSchema) cookie_transport = CookieTransportOauth(cookie_name="rpkapiusersauth") auth_backend = AuthenticationBackend(name="db", transport=cookie_transport, get_strategy=get_database_strategy, ) + google_oauth_router = fastapi_users.get_oauth_router(google_oauth_client, auth_backend, SECRET, is_verified_by_default=True) discord_oauth_router = fastapi_users.get_oauth_router(discord_oauth_client, auth_backend, SECRET, is_verified_by_default=True) diff --git a/api/rpk-api/hub/db.py b/api/rpk-api/hub/db.py index d889cfe..d5a45fe 100644 --- a/api/rpk-api/hub/db.py +++ b/api/rpk-api/hub/db.py @@ -18,4 +18,4 @@ async def init_db(): await init_beanie(database=client.hub, document_models=[User, AccessToken, Firm], - allow_index_dropping=True) \ No newline at end of file + allow_index_dropping=True) diff --git a/api/rpk-api/hub/firm/routes.py b/api/rpk-api/hub/firm/routes.py index e772135..1708d15 100644 --- a/api/rpk-api/hub/firm/routes.py +++ b/api/rpk-api/hub/firm/routes.py @@ -4,44 +4,41 @@ from fastapi import APIRouter, Depends, HTTPException from hub.auth import get_current_user from hub.firm import Firm, FirmRead, FirmCreate, FirmUpdate -model = Firm -model_read = FirmRead -model_create = FirmCreate -model_update = FirmUpdate router = APIRouter() -@router.post("/", response_description="{} added to the database".format(model.__name__)) -async def create(item: model_create, user=Depends(get_current_user)) -> model_read: - exists = await Firm.find_one({"name": item.name, "instance": item.instance}) +@router.post("/", response_description="{} added to the database".format(Firm.__name__)) +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) if exists: raise HTTPException(status_code=400, detail="Firm already exists") - record = model(created_by=user.id, updated_by=user.id, owner=user.id, **item.model_dump()) + record = Firm(created_by=user.id, updated_by=user.id, owner=user.id, **item.model_dump()) o = await record.create() - user.firms.append(o.id) - user.save() - return model_read(**o.model_dump()) + user.firms.append(firm_dict) + await user.save() + return FirmRead(**o.model_dump()) -@router.get("/{id}", response_description="{} record retrieved".format(model.__name__)) -async def read_id(id: PydanticObjectId, user=Depends(get_current_user)) -> model_read: - item = await model.get(id) - return model_read(**item.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) + return FirmRead(**item.model_dump()) -@router.put("/{id}", response_description="{} record updated".format(model.__name__)) -async def update(id: PydanticObjectId, req: model_update, user=Depends(get_current_user)) -> model_read: - item = await model.get(id) +@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(model.__name__) + detail="{} record not found!".format(Firm.__name__) ) if item.owner != user.id: raise HTTPException( status_code=403, - detail="Insufficient credentials to modify {} record".format(model.__name__) + detail="Insufficient credentials to modify {} record".format(Firm.__name__) ) req = {k: v for k, v in req.model_dump().items() if v is not None} @@ -50,23 +47,23 @@ async def update(id: PydanticObjectId, req: model_update, user=Depends(get_curre }} await item.update(update_query) - return model_read(**item.dict()) + return FirmRead(**item.dict()) -@router.delete("/{id}", response_description="{} record deleted from the database".format(model.__name__)) +@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 model.get(id) + item = await Firm.get(id) if not item: raise HTTPException( status_code=404, - detail="{} record not found!".format(model.__name__) + detail="{} record not found!".format(Firm.__name__) ) if item.owner != user.id: raise HTTPException( status_code=403, - detail="Insufficient credentials delete {} record".format(model.__name__) + detail="Insufficient credentials delete {} record".format(Firm.__name__) ) await item.delete() return { - "message": "{} deleted successfully".format(model.__name__) + "message": "{} deleted successfully".format(Firm.__name__) } diff --git a/api/rpk-api/hub/user/__init__.py b/api/rpk-api/hub/user/__init__.py index 3843519..7d6ae8b 100644 --- a/api/rpk-api/hub/user/__init__.py +++ b/api/rpk-api/hub/user/__init__.py @@ -1,13 +1,17 @@ -from beanie import PydanticObjectId -from fastapi_users_db_beanie import BaseOAuthAccount, BeanieUserDatabase, BeanieBaseUserDocument +from beanie import Document +from fastapi_users_db_beanie import BaseOAuthAccount, BeanieUserDatabase, BeanieBaseUser from pydantic import Field +from hub.firm import FirmRead +from hub.user.schemas import UserSchema, UserUpdateSchema + + class OAuthAccount(BaseOAuthAccount): pass -class User(BeanieBaseUserDocument): +class User(BeanieBaseUser, Document): oauth_accounts: list[OAuthAccount] = Field(default_factory=list) - firms: list[PydanticObjectId] = Field(default_factory=list) + firms: list[FirmRead] = Field(default_factory=list) class UserDatabase(BeanieUserDatabase): pass diff --git a/api/rpk-api/hub/user/schemas.py b/api/rpk-api/hub/user/schemas.py index bb41033..d4d515c 100644 --- a/api/rpk-api/hub/user/schemas.py +++ b/api/rpk-api/hub/user/schemas.py @@ -1,7 +1,12 @@ from beanie import PydanticObjectId -from fastapi_users.schemas import BaseUser +from fastapi_users.schemas import BaseUser, BaseUserUpdate from pydantic import Field +from hub.firm import FirmRead + class UserSchema(BaseUser[PydanticObjectId]): - firms: list[PydanticObjectId] = Field() + firms: list[FirmRead] = Field() + +class UserUpdateSchema(BaseUserUpdate): + pass diff --git a/gui/rpk-gui/src/App.tsx b/gui/rpk-gui/src/App.tsx index eba68c9..cdbadfc 100644 --- a/gui/rpk-gui/src/App.tsx +++ b/gui/rpk-gui/src/App.tsx @@ -13,7 +13,7 @@ import routerBindings, { DocumentTitleHandler, UnsavedChangesNotifier, } from "@refinedev/react-router"; -import { BrowserRouter, Outlet, Route, Routes } from "react-router"; +import { BrowserRouter, Link, Outlet, Route, Routes } from "react-router"; import { authProvider } from "./providers/auth-provider"; import { dataProvider } from "./providers/data-provider"; import { ColorModeContextProvider } from "./contexts/color-mode"; @@ -58,7 +58,7 @@ function App() { } /> } /> - HOME} /> + HOME Login} /> } /> } /> } /> diff --git a/gui/rpk-gui/src/components/auth/Logout.tsx b/gui/rpk-gui/src/components/auth/Logout.tsx index ba8b432..1e03830 100644 --- a/gui/rpk-gui/src/components/auth/Logout.tsx +++ b/gui/rpk-gui/src/components/auth/Logout.tsx @@ -1,11 +1,7 @@ -import {Navigate, useSearchParams} from "react-router"; -import {AuthPage} from "@refinedev/mui"; -import GoogleIcon from "@mui/icons-material/Google"; -import DiscordIcon from "../DiscordIcon"; import { useLogout } from "@refinedev/core"; export const Logout = () => { const { mutate: logout } = useLogout(); - return ; + return ; }; diff --git a/gui/rpk-gui/src/components/header/index.tsx b/gui/rpk-gui/src/components/header/index.tsx index 38d3f63..74ca12a 100644 --- a/gui/rpk-gui/src/components/header/index.tsx +++ b/gui/rpk-gui/src/components/header/index.tsx @@ -10,13 +10,8 @@ import { useGetIdentity } from "@refinedev/core"; import { HamburgerMenu, RefineThemedLayoutV2HeaderProps } from "@refinedev/mui"; import React, { useContext } from "react"; import { ColorModeContext } from "../../contexts/color-mode"; -import {Logout} from "../auth/Logout"; - -type IUser = { - id: number; - email: string; - avatar: string; -}; +import { Logout } from "../auth/Logout"; +import { IUser } from "../../interfaces"; export const Header: React.FC = ({ sticky = true, @@ -50,7 +45,7 @@ export const Header: React.FC = ({ {mode === "dark" ? : } - {(user?.avatar || user?.email) && ( + {(user?.email) && ( = ({ {user?.email} )} - + )} diff --git a/gui/rpk-gui/src/interfaces/index.tsx b/gui/rpk-gui/src/interfaces/index.tsx new file mode 100644 index 0000000..3c20e64 --- /dev/null +++ b/gui/rpk-gui/src/interfaces/index.tsx @@ -0,0 +1,13 @@ + +export type IFirm = { + instance: string, + name: string +} + +type User = { + id: number, + email: string, + firms: [IFirm], +}; + +export type IUser = User | null; diff --git a/gui/rpk-gui/src/lib/crud/components/crud-form.tsx b/gui/rpk-gui/src/lib/crud/components/crud-form.tsx index 6bd804c..76935f8 100644 --- a/gui/rpk-gui/src/lib/crud/components/crud-form.tsx +++ b/gui/rpk-gui/src/lib/crud/components/crud-form.tsx @@ -8,10 +8,11 @@ import CrudTextWidget from "./widgets/crud-text-widget"; import UnionEnumField from "./fields/union-enum"; type Props = { - schemaName: string, - resource: string, - id?: string, - //onSubmit: (data: IChangeEvent, event: FormEvent) => void + schemaName: string, + resource: string, + id?: string, + //onSubmit: (data: IChangeEvent, event: FormEvent) => void + onSuccess?: (data: any) => void } const customWidgets: RegistryWidgetsType = { @@ -22,12 +23,13 @@ const customFields: RegistryFieldsType = { AnyOfField: UnionEnumField } -export const CrudForm: React.FC = ({schemaName, resource, id}) => { +export const CrudForm: React.FC = ({ schemaName, resource, id, onSuccess }) => { const { onFinish, query, formLoading } = useForm({ resource: resource, action: id === undefined ? "create" : "edit", redirect: "show", id, + onMutationSuccess: (data: any) => { if (onSuccess) { onSuccess(data) } }, }); const record = query?.data?.data; diff --git a/gui/rpk-gui/src/pages/hub/CreateFirm.tsx b/gui/rpk-gui/src/pages/hub/CreateFirm.tsx index 42e95e1..47dd145 100644 --- a/gui/rpk-gui/src/pages/hub/CreateFirm.tsx +++ b/gui/rpk-gui/src/pages/hub/CreateFirm.tsx @@ -1,7 +1,19 @@ +import { useInvalidateAuthStore } from "@refinedev/core"; import { CrudForm } from "../../lib/crud/components/crud-form"; +import {empty_user} from "../../providers/auth-provider"; export const CreateFirm = () => { + const invalidateAuthStore = useInvalidateAuthStore() + const refreshUser = () => { + empty_user(); + invalidateAuthStore().then(); + } + return ( - + { refreshUser() }} + /> ) } diff --git a/gui/rpk-gui/src/pages/hub/index.tsx b/gui/rpk-gui/src/pages/hub/index.tsx index 2c9ed7c..14d919d 100644 --- a/gui/rpk-gui/src/pages/hub/index.tsx +++ b/gui/rpk-gui/src/pages/hub/index.tsx @@ -1,12 +1,38 @@ import { Button } from "@mui/material"; import { Link } from "react-router"; +import { useGetIdentity } from "@refinedev/core"; + +type Firm = { + name: string, + instance: string, +} +type User = { + firms: [Firm], +} export const Hub = () => { + const user = useGetIdentity(); + + console.log(user); + let ownFirms = []; + let workFirms = []; + //firms.forEach((f, index) => { + // workFirms.push(
  • {f.instance}/{f.name}
  • ) + //}) + //{firms.map((f: Firm, index) => ( + //
  • {f.instance} / {f.name}
  • + // ))} return (

    HUB

    List of managed firms

    -

    List of firm you're working atx

    +
      +
    • +
    +

    List of firm you're working at

    +
      + +
    ); diff --git a/gui/rpk-gui/src/providers/auth-provider.tsx b/gui/rpk-gui/src/providers/auth-provider.tsx index 6a93bf4..c1e9a92 100644 --- a/gui/rpk-gui/src/providers/auth-provider.tsx +++ b/gui/rpk-gui/src/providers/auth-provider.tsx @@ -1,4 +1,6 @@ +import isEmpty from 'lodash/isEmpty'; import { AuthProvider } from "@refinedev/core"; +import {IUser} from "../interfaces"; const API_URL = "/api/v1"; const LOCAL_STORAGE_USER_KEY = "rpk-gui-current-user"; @@ -55,17 +57,24 @@ export const authProvider: AuthProvider = { return { success: false }; }, check: async () => { - return { authenticated: Boolean(get_user()) }; + if (get_user() == null) { + return { + authenticated: false, + redirectTo: "/login", + logout: true + } + } + return { authenticated: true }; }, - getIdentity: async () => { + getIdentity: async (): Promise => { const user = get_user(); - if (user != null) { + if (user !== null && !isEmpty(user)) { return user; } const response = await fetch(`${API_URL}/users/me`); if (response.status < 200 || response.status > 299) { - return + return null; } const user_data = await response.json(); store_user(user_data) @@ -163,6 +172,10 @@ function forget_user() { localStorage.removeItem(LOCAL_STORAGE_USER_KEY); } +export function empty_user() { + store_user({}) +} + function findGetParameter(parameterName: string) { let result = null, tmp = []; location.search.substr(1).split("&")