mirror of
https://github.com/QuivrHQ/quivr.git
synced 2025-01-07 16:21:00 +03:00
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:
parent
b3455d3686
commit
d27504f735
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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");
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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 }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
5
frontend/lib/api/brain/types.ts
Normal file
5
frontend/lib/api/brain/types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { BrainRoleType } from "@/lib/components/NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
|
||||||
|
|
||||||
|
export type SubscriptionUpdatableProperties = {
|
||||||
|
role: BrainRoleType | null;
|
||||||
|
};
|
@ -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 = () => {
|
||||||
|
@ -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,
|
||||||
|
});
|
@ -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,
|
||||||
|
});
|
@ -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,
|
||||||
|
});
|
@ -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,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -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 (
|
||||||
|
@ -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} />
|
||||||
|
@ -39,7 +39,7 @@ vi.mock("@/lib/context/BrainProvider/hooks/useBrainContext", async () => {
|
|||||||
useBrainContext: () => ({
|
useBrainContext: () => ({
|
||||||
...actual.useBrainContext(),
|
...actual.useBrainContext(),
|
||||||
currentBrain: {
|
currentBrain: {
|
||||||
rights: "Editor",
|
role: "Editor",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
|
@ -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(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user