Compare commits

..

2 Commits

15 changed files with 144 additions and 86 deletions

View File

@@ -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)

View File

@@ -22,7 +22,8 @@ class FirmRead(BaseModel):
name: str = Field()
class FirmCreate(FirmRead):
pass
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-]+$")
class FirmUpdate(FirmRead):
pass
class FirmUpdate(BaseModel):
owner: PydanticObjectId = Field()

View File

@@ -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)) -> dict:
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")
item.created_by = user.id
item.updated_by = user.id
item.owner = user.id
o = await model(**item.model_dump()).create()
return model_read(**o.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(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__)
}

View File

@@ -1,12 +1,17 @@
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[FirmRead] = Field(default_factory=list)
class UserDatabase(BeanieUserDatabase):
pass

View File

@@ -1,5 +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[FirmRead] = Field()
class UserUpdateSchema(BaseUserUpdate):
pass

View File

@@ -13,9 +13,9 @@ import routerBindings, {
DocumentTitleHandler,
UnsavedChangesNotifier,
} from "@refinedev/react-router";
import dataProvider from "@refinedev/simple-rest";
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";
import { Login } from "./components/auth/Login";
import { Register } from "./components/auth/Register";
@@ -36,7 +36,7 @@ function App() {
<RefineSnackbarProvider>
<Refine
authProvider={authProvider}
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
dataProvider={dataProvider}
notificationProvider={useNotificationProvider}
routerProvider={routerBindings}
options={{
@@ -58,7 +58,7 @@ function App() {
<Route path="/hub" element={ <Hub /> } />
<Route path="/hub/create-firm" element={ <CreateFirm /> } />
</Route>
<Route index element={<h1>HOME</h1>} />
<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 />} />

View File

@@ -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 <button onClick={() => logout()}>Logout</button>;
return <button onClick={() => logout()} >Logout</button>;
};

View File

@@ -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<RefineThemedLayoutV2HeaderProps> = ({
sticky = true,
@@ -50,7 +45,7 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
{mode === "dark" ? <LightModeOutlined /> : <DarkModeOutlined />}
</IconButton>
{(user?.avatar || user?.email) && (
{(user?.email) && (
<Stack
direction="row"
gap="16px"
@@ -70,7 +65,7 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
{user?.email}
</Typography>
)}
<Avatar src={user?.avatar} alt={user?.email} />
<Avatar src={"user?.avatar"} alt={user?.email} />
<Logout />
</Stack>
)}

View File

@@ -0,0 +1,13 @@
export type IFirm = {
instance: string,
name: string
}
type User = {
id: number,
email: string,
firms: [IFirm],
};
export type IUser = User | null;

View File

@@ -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<any>) => void
schemaName: string,
resource: string,
id?: string,
//onSubmit: (data: IChangeEvent, event: FormEvent<any>) => void
onSuccess?: (data: any) => void
}
const customWidgets: RegistryWidgetsType = {
@@ -22,12 +23,13 @@ const customFields: RegistryFieldsType = {
AnyOfField: UnionEnumField
}
export const CrudForm: React.FC<Props> = ({schemaName, resource, id}) => {
export const CrudForm: React.FC<Props> = ({ 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;

View File

@@ -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 (
<CrudForm schemaName={"FirmCreate"} resource={"firms"} />
<CrudForm
schemaName={"FirmCreate"}
resource={"firms"}
onSuccess={() => { refreshUser() }}
/>
)
}

View File

@@ -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<User>();
console.log(user);
let ownFirms = [];
let workFirms = [];
//firms.forEach((f, index) => {
// workFirms.push(<li>{f.instance}/{f.name}</li>)
//})
//{firms.map((f: Firm, index) => (
// <li key={index}>{f.instance} / {f.name}</li>
// ))}
return (
<div>
<h1>HUB</h1>
<p>List of managed firms</p>
<p>List of firm you're working atx</p>
<ul>
<li></li>
</ul>
<p>List of firm you're working at</p>
<ul>
</ul>
<Link to="/hub/create-firm" ><Button >Create a new firm</Button></Link>
</div>
);

View File

@@ -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<IUser> => {
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("&")

View File

@@ -1,20 +1,10 @@
import type { DataProvider } from "@refinedev/core";
const API_URL = "http://localhost:8000";
const fetcher = async (url: string, options?: RequestInit) => {
return fetch(url, {
...options,
headers: {
...options?.headers,
Authorization: "Bearer " + localStorage.getItem("access_token"),
},
});
};
const API_URL = "/api/v1";
export const dataProvider: DataProvider = {
getOne: async ({ resource, id, meta }) => {
const response = id !== "" ? await fetcher(`${API_URL}/${resource}/${id}`) : await fetcher(`${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;
const data = await response.json();
@@ -24,7 +14,7 @@ export const dataProvider: DataProvider = {
};
},
update: async ({ resource, id, variables }) => {
const response = await fetcher(`${API_URL}/${resource}/${id}`, {
const response = await fetch(`${API_URL}/${resource}/${id}`, {
method: "PUT",
body: JSON.stringify(variables),
headers: {
@@ -58,7 +48,7 @@ export const dataProvider: DataProvider = {
});
}
const response = await fetcher(`${API_URL}/${resource}?${params.toString()}`);
const response = await fetch(`${API_URL}/${resource}?${params.toString()}`);
if (response.status < 200 || response.status > 299) throw response;
@@ -70,7 +60,7 @@ export const dataProvider: DataProvider = {
};
},
create: async ({ resource, variables }) => {
const response = await fetcher(`${API_URL}/${resource}`, {
const response = await fetch(`${API_URL}/${resource}`, {
method: "POST",
body: JSON.stringify(variables),
headers: {
@@ -85,7 +75,7 @@ export const dataProvider: DataProvider = {
return { data };
},
deleteOne: async ({ resource, id, variables, meta }) => {
const response = await fetcher(`${API_URL}/${resource}/${id}`,{
const response = await fetch(`${API_URL}/${resource}/${id}`,{
method: "DELETE",
});