Feat/rate limiting (#719)

* feat: add max brain count

* fix: prevent page cashing when invitation is invalid

* feat: rename rights to role in frontend
This commit is contained in:
Mamadou DICKO 2023-07-20 18:17:55 +02:00 committed by GitHub
parent b3455d3686
commit d27504f735
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 215 additions and 73 deletions

View File

@ -6,8 +6,10 @@ JWT_SECRET_KEY=<change-me>
AUTHENTICATE=true AUTHENTICATE=true
GOOGLE_APPLICATION_CREDENTIALS=<change-me> GOOGLE_APPLICATION_CREDENTIALS=<change-me>
GOOGLE_CLOUD_PROJECT=<change-me> GOOGLE_CLOUD_PROJECT=<change-me>
MAX_BRAIN_SIZE=52428800 MAX_BRAIN_SIZE=52428800
MAX_REQUESTS_NUMBER=200 MAX_REQUESTS_NUMBER=200
MAX_BRAIN_PER_USER=5
#Private LLM Variables #Private LLM Variables
PRIVATE=False PRIVATE=False

View File

@ -1,4 +1,3 @@
import os
from typing import Any, List, Optional from typing import Any, List, Optional
from uuid import UUID from uuid import UUID
@ -6,7 +5,7 @@ from logger import get_logger
from pydantic import BaseModel from pydantic import BaseModel
from utils.vectors import get_unique_files_from_vector_ids from utils.vectors import get_unique_files_from_vector_ids
from models.settings import CommonsDep, common_dependencies from models.settings import BrainRateLimiting, CommonsDep, common_dependencies
from models.users import User from models.users import User
logger = get_logger(__name__) logger = get_logger(__name__)
@ -19,12 +18,16 @@ class Brain(BaseModel):
model: Optional[str] = "gpt-3.5-turbo-0613" model: Optional[str] = "gpt-3.5-turbo-0613"
temperature: Optional[float] = 0.0 temperature: Optional[float] = 0.0
max_tokens: Optional[int] = 256 max_tokens: Optional[int] = 256
max_brain_size: Optional[int] = int(os.getenv("MAX_BRAIN_SIZE", 52428800))
files: List[Any] = [] files: List[Any] = []
class Config: class Config:
arbitrary_types_allowed = True arbitrary_types_allowed = True
@property
def max_brain_size(self) -> int:
brain_rate_limiting = BrainRateLimiting()
return brain_rate_limiting.max_brain_size
@property @property
def commons(self) -> CommonsDep: def commons(self) -> CommonsDep:
return common_dependencies() return common_dependencies()

View File

@ -7,6 +7,11 @@ from supabase.client import Client, create_client
from vectorstore.supabase import SupabaseVectorStore from vectorstore.supabase import SupabaseVectorStore
class BrainRateLimiting(BaseSettings):
max_brain_size = 52428800
max_brain_per_user = 5
class BrainSettings(BaseSettings): class BrainSettings(BaseSettings):
openai_api_key: str openai_api_key: str
anthropic_api_key: str anthropic_api_key: str

View File

@ -8,7 +8,7 @@ from models.brains import (
get_default_user_brain, get_default_user_brain,
get_default_user_brain_or_create_new, get_default_user_brain_or_create_new,
) )
from models.settings import common_dependencies from models.settings import BrainRateLimiting, common_dependencies
from models.users import User from models.users import User
from routes.authorizations.brain_authorization import has_brain_authorization from routes.authorizations.brain_authorization import has_brain_authorization
@ -104,6 +104,15 @@ async def create_brain_endpoint(
brain = Brain(name=brain.name) # pyright: ignore reportPrivateUsage=none brain = Brain(name=brain.name) # pyright: ignore reportPrivateUsage=none
user_brains = brain.get_user_brains(current_user.id)
max_brain_per_user = BrainRateLimiting().max_brain_per_user
if len(user_brains) >= max_brain_per_user:
raise HTTPException(
status_code=429,
detail=f"Maximum number of brains reached ({max_brain_per_user}).",
)
brain.create_brain() # pyright: ignore reportPrivateUsage=none brain.create_brain() # pyright: ignore reportPrivateUsage=none
default_brain = get_default_user_brain(current_user) default_brain = get_default_user_brain(current_user)
if default_brain: if default_brain:

View File

@ -4,6 +4,7 @@ import time
from auth import AuthBearer, get_current_user from auth import AuthBearer, get_current_user
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from models.brains import Brain, get_default_user_brain from models.brains import Brain, get_default_user_brain
from models.settings import BrainRateLimiting
from models.users import User from models.users import User
user_router = APIRouter() user_router = APIRouter()
@ -32,7 +33,8 @@ async def get_user_endpoint(
information about the user's API usage. information about the user's API usage.
""" """
max_brain_size = int(os.getenv("MAX_BRAIN_SIZE", 52428800)) max_brain_size = BrainRateLimiting().max_brain_size
if request.headers.get("Openai-Api-Key"): if request.headers.get("Openai-Api-Key"):
max_brain_size = MAX_BRAIN_SIZE_WITH_OWN_KEY max_brain_size = MAX_BRAIN_SIZE_WITH_OWN_KEY

View File

@ -34,7 +34,7 @@ const DocumentItem = forwardRef(
const { track } = useEventTracking(); const { track } = useEventTracking();
const { currentBrain } = useBrainContext(); const { currentBrain } = useBrainContext();
const canDeleteFile = currentBrain?.rights === "Owner"; const canDeleteFile = currentBrain?.role === "Owner";
if (!session) { if (!session) {
throw new Error("User session not found"); throw new Error("User session not found");

View File

@ -17,7 +17,7 @@ export const useInvitation = () => {
const brainId = params?.brainId as UUID | undefined; const brainId = params?.brainId as UUID | undefined;
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [brainName, setBrainName] = useState<string>(""); const [brainName, setBrainName] = useState<string>("");
const [rights, setRights] = useState<BrainRoleType | undefined>(); const [role, setRole] = useState<BrainRoleType | undefined>();
const [isProcessingRequest, setIsProcessingRequest] = useState(false); const [isProcessingRequest, setIsProcessingRequest] = useState(false);
const { publish } = useToast(); const { publish } = useToast();
@ -37,9 +37,9 @@ export const useInvitation = () => {
const checkInvitationValidity = async () => { const checkInvitationValidity = async () => {
try { try {
const { name, rights: assignedRights } = await getInvitation(brainId); const { name, role: assignedRole } = await getInvitation(brainId);
setBrainName(name); setBrainName(name);
setRights(assignedRights); setRole(assignedRole);
} catch (error) { } catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) { if (axios.isAxiosError(error) && error.response?.status === 404) {
publish({ publish({
@ -130,7 +130,7 @@ export const useInvitation = () => {
handleAccept, handleAccept,
handleDecline, handleDecline,
brainName, brainName,
rights, role,
isLoading, isLoading,
isProcessingRequest, isProcessingRequest,
}; };

View File

@ -15,7 +15,7 @@ const InvitationPage = (): JSX.Element => {
handleDecline, handleDecline,
isLoading, isLoading,
brainName, brainName,
rights, role,
} = useInvitation(); } = useInvitation();
const { session } = useSupabase(); const { session } = useSupabase();
@ -27,15 +27,18 @@ const InvitationPage = (): JSX.Element => {
redirectToLogin(); redirectToLogin();
} }
if (rights === undefined) { if (role === undefined) {
throw new Error("Rights are undefined"); // This should never happen
// It is a way to prevent the page from crashing when invitation is invalid instead of throwing an error
// The user will be redirected to the home page (handled in the useInvitation hook)
return <div />;
} }
return ( return (
<main className="pt-10"> <main className="pt-10">
<PageHeading <PageHeading
title={`Welcome to ${brainName}!`} title={`Welcome to ${brainName}!`}
subtitle={`You have been invited to join this brain as a ${rights} and start exploring. Do you accept this exciting journey?`} subtitle={`You have been invited to join this brain as a ${role} and start exploring. Do you accept this exciting journey?`}
/> />
{isProcessingRequest ? ( {isProcessingRequest ? (
<div className="flex flex-col items-center justify-center mt-5"> <div className="flex flex-col items-center justify-center mt-5">

View File

@ -35,7 +35,7 @@ const UploadPage = (): JSX.Element => {
); );
} }
const hasUploadRights = requiredRolesForUpload.includes(currentBrain.rights); const hasUploadRights = requiredRolesForUpload.includes(currentBrain.role);
if (!hasUploadRights) { if (!hasUploadRights) {
return ( return (
@ -44,7 +44,7 @@ const UploadPage = (): JSX.Element => {
<strong className="font-bold mr-1">Oh no!</strong> <strong className="font-bold mr-1">Oh no!</strong>
<span className="block sm:inline"> <span className="block sm:inline">
{ {
"You don't have the necessary rights to upload content to the selected brain. 🧠💡🥲" "You don't have the necessary role to upload content to the selected brain. 🧠💡🥲"
} }
</span> </span>
</div> </div>

View File

@ -2,7 +2,8 @@
import { renderHook } from "@testing-library/react"; import { renderHook } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { Subscription, SubscriptionUpdatableProperties } from "../brain"; import { Subscription } from "../brain";
import { SubscriptionUpdatableProperties } from "../types";
import { useBrainApi } from "../useBrainApi"; import { useBrainApi } from "../useBrainApi";
const axiosGetMock = vi.fn(() => ({ const axiosGetMock = vi.fn(() => ({
@ -89,6 +90,12 @@ describe("useBrainApi", () => {
}); });
it("should call getBrains with the correct parameters", async () => { it("should call getBrains with the correct parameters", async () => {
axiosGetMock.mockImplementationOnce(() => ({
data: {
//@ts-ignore we don't really need returned value here
brains: [],
},
}));
const { const {
result: { result: {
current: { getBrains }, current: { getBrains },
@ -123,19 +130,26 @@ describe("useBrainApi", () => {
const subscriptions: Subscription[] = [ const subscriptions: Subscription[] = [
{ {
email: "user@quivr.app", email: "user@quivr.app",
rights: "Viewer", role: "Viewer",
}, },
]; ];
await addBrainSubscriptions(id, subscriptions); await addBrainSubscriptions(id, subscriptions);
expect(axiosPostMock).toHaveBeenCalledTimes(1); expect(axiosPostMock).toHaveBeenCalledTimes(1);
expect(axiosPostMock).toHaveBeenCalledWith( expect(axiosPostMock).toHaveBeenCalledWith(`/brains/${id}/subscription`, [
`/brains/${id}/subscription`, {
subscriptions email: "user@quivr.app",
); rights: "Viewer",
},
]);
}); });
it("should call getBrainUsers with the correct parameters", async () => { it("should call getBrainUsers with the correct parameters", async () => {
axiosGetMock.mockImplementationOnce(() => ({
//@ts-ignore we don't really need returned value here
data: [],
}));
const { const {
result: { result: {
current: { getBrainUsers }, current: { getBrainUsers },
@ -156,13 +170,13 @@ describe("useBrainApi", () => {
const brainId = "123"; const brainId = "123";
const email = "456"; const email = "456";
const subscription: SubscriptionUpdatableProperties = { const subscription: SubscriptionUpdatableProperties = {
rights: "Viewer", role: "Viewer",
}; };
await updateBrainAccess(brainId, email, subscription); await updateBrainAccess(brainId, email, subscription);
expect(axiosPutMock).toHaveBeenCalledTimes(1); expect(axiosPutMock).toHaveBeenCalledTimes(1);
expect(axiosPutMock).toHaveBeenCalledWith( expect(axiosPutMock).toHaveBeenCalledWith(
`/brains/${brainId}/subscription`, `/brains/${brainId}/subscription`,
{ ...subscription, email } { rights: "Viewer", email }
); );
}); });
}); });

View File

@ -1,9 +1,22 @@
/* eslint-disable max-lines */
import { AxiosInstance } from "axios"; import { AxiosInstance } from "axios";
import { BrainRoleType } from "@/lib/components/NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types"; import { BrainRoleType } from "@/lib/components/NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
import { Brain, MinimalBrainForUser } from "@/lib/context/BrainProvider/types"; import {
BackendMinimalBrainForUser,
Brain,
MinimalBrainForUser,
} from "@/lib/context/BrainProvider/types";
import { Document } from "@/lib/types/Document"; import { Document } from "@/lib/types/Document";
import { SubscriptionUpdatableProperties } from "./types";
import { mapBackendMinimalBrainToMinimalBrain } from "./utils/mapBackendMinimalBrainToMinimalBrain";
import {
BackendSubscription,
mapSubscriptionToBackendSubscription,
} from "./utils/mapSubscriptionToBackendSubscription";
import { mapSubscriptionUpdatablePropertiesToBackendSubscriptionUpdatableProperties } from "./utils/mapSubscriptionUpdatablePropertiesToBackendSubscriptionUpdatableProperties";
export const getBrainDocuments = async ( export const getBrainDocuments = async (
brainId: string, brainId: string,
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
@ -19,11 +32,10 @@ export const createBrain = async (
name: string, name: string,
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
): Promise<MinimalBrainForUser> => { ): Promise<MinimalBrainForUser> => {
const createdBrain = ( return mapBackendMinimalBrainToMinimalBrain(
await axiosInstance.post<MinimalBrainForUser>(`/brains/`, { name }) (await axiosInstance.post<BackendMinimalBrainForUser>(`/brains/`, { name }))
).data; .data
);
return createdBrain;
}; };
export const getBrain = async ( export const getBrain = async (
@ -47,40 +59,49 @@ export const deleteBrain = async (
export const getDefaultBrain = async ( export const getDefaultBrain = async (
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
): Promise<MinimalBrainForUser | undefined> => { ): Promise<MinimalBrainForUser | undefined> => {
return (await axiosInstance.get<MinimalBrainForUser>(`/brains/default/`)) return mapBackendMinimalBrainToMinimalBrain(
.data; (await axiosInstance.get<BackendMinimalBrainForUser>(`/brains/default/`))
.data
);
}; };
export const getBrains = async ( export const getBrains = async (
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
): Promise<MinimalBrainForUser[]> => { ): Promise<MinimalBrainForUser[]> => {
const brains = ( const { brains } = (
await axiosInstance.get<{ brains: MinimalBrainForUser[] }>(`/brains/`) await axiosInstance.get<{ brains: BackendMinimalBrainForUser[] }>(
`/brains/`
)
).data; ).data;
return brains.brains; return brains.map(mapBackendMinimalBrainToMinimalBrain);
}; };
export type Subscription = { email: string; rights: BrainRoleType }; export type Subscription = { email: string; role: BrainRoleType };
export const addBrainSubscriptions = async ( export const addBrainSubscriptions = async (
brainId: string, brainId: string,
subscriptions: Subscription[], subscriptions: Subscription[],
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
): Promise<void> => { ): Promise<void> => {
await axiosInstance.post(`/brains/${brainId}/subscription`, subscriptions); await axiosInstance.post(
`/brains/${brainId}/subscription`,
subscriptions.map(mapSubscriptionToBackendSubscription)
);
}; };
export const getBrainUsers = async ( export const getBrainUsers = async (
brainId: string, brainId: string,
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
): Promise<Subscription[]> => { ): Promise<Subscription[]> => {
return (await axiosInstance.get<Subscription[]>(`/brains/${brainId}/users`)) const brainsUsers = (
.data; await axiosInstance.get<BackendSubscription[]>(`/brains/${brainId}/users`)
}; ).data;
export type SubscriptionUpdatableProperties = { return brainsUsers.map((brainUser) => ({
rights: BrainRoleType | null; email: brainUser.email,
role: brainUser.rights,
}));
}; };
export const updateBrainAccess = async ( export const updateBrainAccess = async (
@ -90,7 +111,9 @@ export const updateBrainAccess = async (
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
): Promise<void> => { ): Promise<void> => {
await axiosInstance.put(`/brains/${brainId}/subscription`, { await axiosInstance.put(`/brains/${brainId}/subscription`, {
...subscription, ...mapSubscriptionUpdatablePropertiesToBackendSubscriptionUpdatableProperties(
subscription
),
email: userEmail, email: userEmail,
}); });
}; };

View File

@ -0,0 +1,5 @@
import { BrainRoleType } from "@/lib/components/NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
export type SubscriptionUpdatableProperties = {
role: BrainRoleType | null;
};

View File

@ -10,9 +10,9 @@ import {
getBrainUsers, getBrainUsers,
getDefaultBrain, getDefaultBrain,
Subscription, Subscription,
SubscriptionUpdatableProperties,
updateBrainAccess, updateBrainAccess,
} from "./brain"; } from "./brain";
import { SubscriptionUpdatableProperties } from "./types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainApi = () => { export const useBrainApi = () => {

View File

@ -0,0 +1,12 @@
import {
BackendMinimalBrainForUser,
MinimalBrainForUser,
} from "@/lib/context/BrainProvider/types";
export const mapBackendMinimalBrainToMinimalBrain = (
backendMinimalBrain: BackendMinimalBrainForUser
): MinimalBrainForUser => ({
id: backendMinimalBrain.id,
name: backendMinimalBrain.name,
role: backendMinimalBrain.rights,
});

View File

@ -0,0 +1,12 @@
import { BrainRoleType } from "@/lib/components/NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
import { Subscription } from "../brain";
export type BackendSubscription = { email: string; rights: BrainRoleType };
export const mapSubscriptionToBackendSubscription = (
subscription: Subscription
): BackendSubscription => ({
email: subscription.email,
rights: subscription.role,
});

View File

@ -0,0 +1,13 @@
import { BrainRoleType } from "@/lib/components/NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
import { SubscriptionUpdatableProperties } from "../types";
type BackendSubscriptionUpdatableProperties = {
rights: BrainRoleType | null;
};
export const mapSubscriptionUpdatablePropertiesToBackendSubscriptionUpdatableProperties =
(
subscriptionUpdatableProperties: SubscriptionUpdatableProperties
): BackendSubscriptionUpdatableProperties => ({
rights: subscriptionUpdatableProperties.role,
});

View File

@ -13,8 +13,6 @@ export const acceptInvitation = async (
) )
).data; ).data;
console.log("acceptedInvitation", acceptedInvitation);
return acceptedInvitation; return acceptedInvitation;
}; };
@ -33,6 +31,11 @@ export const declineInvitation = async (
export type InvitationBrain = { export type InvitationBrain = {
name: string; name: string;
role: BrainRoleType;
};
//TODO: rename rights to role in Backend and use InvitationBrain instead of BackendInvitationBrain
type BackendInvitationBrain = Omit<InvitationBrain, "role"> & {
rights: BrainRoleType; rights: BrainRoleType;
}; };
@ -40,7 +43,14 @@ export const getInvitation = async (
brainId: UUID, brainId: UUID,
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
): Promise<InvitationBrain> => { ): Promise<InvitationBrain> => {
return ( const invitation = (
await axiosInstance.get<InvitationBrain>(`/brains/${brainId}/subscription`) await axiosInstance.get<BackendInvitationBrain>(
`/brains/${brainId}/subscription`
)
).data; ).data;
return {
name: invitation.name,
role: invitation.rights,
};
}; };

View File

@ -1,3 +1,4 @@
import axios from "axios";
import { FormEvent, useState } from "react"; import { FormEvent, useState } from "react";
import { MdAdd } from "react-icons/md"; import { MdAdd } from "react-icons/md";
@ -5,11 +6,12 @@ import Button from "@/lib/components/ui/Button";
import Field from "@/lib/components/ui/Field"; import Field from "@/lib/components/ui/Field";
import { Modal } from "@/lib/components/ui/Modal"; import { Modal } from "@/lib/components/ui/Modal";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext"; import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useToast } from "@/lib/hooks";
export const AddBrainModal = (): JSX.Element => { export const AddBrainModal = (): JSX.Element => {
const [newBrainName, setNewBrainName] = useState(""); const [newBrainName, setNewBrainName] = useState("");
const [isPending, setIsPending] = useState(false); const [isPending, setIsPending] = useState(false);
const { publish } = useToast();
const { createBrain } = useBrainContext(); const { createBrain } = useBrainContext();
const handleSubmit = async (e: FormEvent) => { const handleSubmit = async (e: FormEvent) => {
@ -17,10 +19,31 @@ export const AddBrainModal = (): JSX.Element => {
if (newBrainName.trim() === "" || isPending) { if (newBrainName.trim() === "" || isPending) {
return; return;
} }
try {
setIsPending(true); setIsPending(true);
await createBrain(newBrainName); await createBrain(newBrainName);
setNewBrainName(""); setNewBrainName("");
} catch (err) {
if (axios.isAxiosError(err) && err.response?.status === 429) {
publish({
variant: "danger",
text: `${JSON.stringify(
(
err.response as {
data: { detail: string };
}
).data.detail
)}`,
});
} else {
publish({
variant: "danger",
text: `${JSON.stringify(err)}`,
});
}
} finally {
setIsPending(false); setIsPending(false);
}
}; };
return ( return (

View File

@ -12,7 +12,7 @@ const requiredAccessToShareBrain: BrainRoleType[] = ["Owner", "Editor"];
export const BrainActions = ({ brain }: BrainActionsProps): JSX.Element => { export const BrainActions = ({ brain }: BrainActionsProps): JSX.Element => {
return ( return (
<div className="absolute right-0 flex flex-row"> <div className="absolute right-0 flex flex-row">
{requiredAccessToShareBrain.includes(brain.rights) && ( {requiredAccessToShareBrain.includes(brain.role) && (
<ShareBrain brainId={brain.id} name={brain.name} /> <ShareBrain brainId={brain.id} name={brain.name} />
)} )}
<DeleteBrain brainId={brain.id} /> <DeleteBrain brainId={brain.id} />

View File

@ -39,7 +39,7 @@ vi.mock("@/lib/context/BrainProvider/hooks/useBrainContext", async () => {
useBrainContext: () => ({ useBrainContext: () => ({
...actual.useBrainContext(), ...actual.useBrainContext(),
currentBrain: { currentBrain: {
rights: "Editor", role: "Editor",
}, },
}), }),
}; };

View File

@ -23,7 +23,7 @@ export const BrainUsers = ({ brainId }: BrainUsersProps): JSX.Element => {
<BrainUser <BrainUser
key={subscription.email} key={subscription.email}
email={subscription.email} email={subscription.email}
rights={subscription.rights} role={subscription.role}
brainId={brainId} brainId={brainId}
fetchBrainUsers={fetchBrainUsers} fetchBrainUsers={fetchBrainUsers}
/> />

View File

@ -11,14 +11,14 @@ import { availableRoles } from "../../../../types";
type BrainUserProps = { type BrainUserProps = {
email: string; email: string;
rights: BrainRoleType; role: BrainRoleType;
brainId: string; brainId: string;
fetchBrainUsers: () => Promise<void>; fetchBrainUsers: () => Promise<void>;
}; };
export const BrainUser = ({ export const BrainUser = ({
email, email,
rights, role,
brainId, brainId,
fetchBrainUsers, fetchBrainUsers,
}: BrainUserProps): JSX.Element => { }: BrainUserProps): JSX.Element => {
@ -30,7 +30,7 @@ export const BrainUser = ({
updateSelectedRole, updateSelectedRole,
} = useBrainUser({ } = useBrainUser({
fetchBrainUsers: fetchBrainUsers, fetchBrainUsers: fetchBrainUsers,
rights, role,
brainId, brainId,
email, email,
}); });
@ -62,7 +62,7 @@ export const BrainUser = ({
onChange={(newRole) => void updateSelectedRole(newRole)} onChange={(newRole) => void updateSelectedRole(newRole)}
value={selectedRole} value={selectedRole}
options={availableRoles} options={availableRoles}
readOnly={currentBrain?.rights !== "Owner" && selectedRole === "Owner"} readOnly={currentBrain?.role !== "Owner" && selectedRole === "Owner"}
/> />
</div> </div>
); );

View File

@ -9,7 +9,7 @@ import { BrainRoleType } from "../../../../../../../types";
type UseBrainUserProps = { type UseBrainUserProps = {
fetchBrainUsers: () => Promise<void>; fetchBrainUsers: () => Promise<void>;
rights: BrainRoleType; role: BrainRoleType;
brainId: string; brainId: string;
email: string; email: string;
}; };
@ -17,19 +17,19 @@ type UseBrainUserProps = {
export const useBrainUser = ({ export const useBrainUser = ({
brainId, brainId,
fetchBrainUsers, fetchBrainUsers,
rights, role,
email, email,
}: UseBrainUserProps) => { }: UseBrainUserProps) => {
const { updateBrainAccess } = useBrainApi(); const { updateBrainAccess } = useBrainApi();
const { publish } = useToast(); const { publish } = useToast();
const [selectedRole, setSelectedRole] = useState<BrainRoleType>(rights); const [selectedRole, setSelectedRole] = useState<BrainRoleType>(role);
const [isRemovingAccess, setIsRemovingAccess] = useState(false); const [isRemovingAccess, setIsRemovingAccess] = useState(false);
const { currentBrain } = useBrainContext(); const { currentBrain } = useBrainContext();
const updateSelectedRole = async (newRole: BrainRoleType) => { const updateSelectedRole = async (newRole: BrainRoleType) => {
setSelectedRole(newRole); setSelectedRole(newRole);
try { try {
await updateBrainAccess(brainId, email, { await updateBrainAccess(brainId, email, {
rights: newRole, role: newRole,
}); });
publish({ variant: "success", text: `Updated ${email} to ${newRole}` }); publish({ variant: "success", text: `Updated ${email} to ${newRole}` });
void fetchBrainUsers(); void fetchBrainUsers();
@ -58,7 +58,7 @@ export const useBrainUser = ({
setIsRemovingAccess(true); setIsRemovingAccess(true);
try { try {
await updateBrainAccess(brainId, email, { await updateBrainAccess(brainId, email, {
rights: null, role: null,
}); });
publish({ variant: "success", text: `Removed ${email} from brain` }); publish({ variant: "success", text: `Removed ${email} from brain` });
void fetchBrainUsers(); void fetchBrainUsers();
@ -82,7 +82,7 @@ export const useBrainUser = ({
setIsRemovingAccess(false); setIsRemovingAccess(false);
} }
}; };
const canRemoveAccess = currentBrain?.rights === "Owner"; const canRemoveAccess = currentBrain?.role === "Owner";
return { return {
isRemovingAccess, isRemovingAccess,

View File

@ -20,7 +20,7 @@ export const UserToInvite = ({
roleAssignation, roleAssignation,
}: UserToInviteProps): JSX.Element => { }: UserToInviteProps): JSX.Element => {
const [selectedRole, setSelectedRole] = useState<BrainRoleType>( const [selectedRole, setSelectedRole] = useState<BrainRoleType>(
roleAssignation.rights roleAssignation.role
); );
const [email, setEmail] = useState(roleAssignation.email); const [email, setEmail] = useState(roleAssignation.email);
const { currentBrain } = useBrainContext(); const { currentBrain } = useBrainContext();
@ -33,7 +33,7 @@ export const UserToInvite = ({
onChange({ onChange({
...roleAssignation, ...roleAssignation,
email, email,
rights: selectedRole, role: selectedRole,
}); });
}, [email, selectedRole]); }, [email, selectedRole]);
@ -60,7 +60,7 @@ export const UserToInvite = ({
<Select <Select
onChange={setSelectedRole} onChange={setSelectedRole}
value={selectedRole} value={selectedRole}
options={userRoleToAssignableRoles[currentBrain.rights]} options={userRoleToAssignableRoles[currentBrain.role]}
/> />
</div> </div>
); );

View File

@ -2,6 +2,7 @@
import axios, { AxiosResponse } from "axios"; import axios, { AxiosResponse } from "axios";
import { useState } from "react"; import { useState } from "react";
import { Subscription } from "@/lib/api/brain/brain";
import { useBrainApi } from "@/lib/api/brain/useBrainApi"; import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import { useToast } from "@/lib/hooks"; import { useToast } from "@/lib/hooks";
@ -61,11 +62,11 @@ export const useShareBrain = (brainId: string) => {
const inviteUsers = async (): Promise<void> => { const inviteUsers = async (): Promise<void> => {
setSendingInvitation(true); setSendingInvitation(true);
try { try {
const inviteUsersPayload = roleAssignations const inviteUsersPayload: Subscription[] = roleAssignations
.filter(({ email }) => email !== "") .filter(({ email }) => email !== "")
.map((assignation) => ({ .map((assignation) => ({
email: assignation.email, email: assignation.email,
rights: assignation.rights, role: assignation.role,
})); }));
await addBrainSubscriptions(brainId, inviteUsersPayload); await addBrainSubscriptions(brainId, inviteUsersPayload);

View File

@ -3,7 +3,7 @@ import { BrainRoleAssignation } from "../../../types";
export const generateBrainAssignation = (): BrainRoleAssignation => { export const generateBrainAssignation = (): BrainRoleAssignation => {
return { return {
email: "", email: "",
rights: "Viewer", role: "Viewer",
id: Math.random().toString(), id: Math.random().toString(),
}; };
}; };

View File

@ -4,6 +4,6 @@ export type BrainRoleType = (typeof roles)[number];
export type BrainRoleAssignation = { export type BrainRoleAssignation = {
email: string; email: string;
rights: BrainRoleType; role: BrainRoleType;
id: string; id: string;
}; };

View File

@ -18,6 +18,11 @@ export type Brain = {
export type MinimalBrainForUser = { export type MinimalBrainForUser = {
id: UUID; id: UUID;
name: string; name: string;
role: BrainRoleType;
};
//TODO: rename rights to role in Backend and use MinimalBrainForUser instead of BackendMinimalBrainForUser
export type BackendMinimalBrainForUser = Omit<MinimalBrainForUser, "role"> & {
rights: BrainRoleType; rights: BrainRoleType;
}; };