Shareable brain 9 (#677)

* feat: add 20230717173000_add_get_user_id_by_user_email

* feat(ShareableBrain): add update access endpoint

* feat(sdk): add updateBrainAccess

* feat: add brain access control

* feat: improve ux
This commit is contained in:
Mamadou DICKO 2023-07-18 14:30:19 +02:00 committed by GitHub
parent 260e20d401
commit 81b57c504a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 280 additions and 93 deletions

View File

@ -3,11 +3,12 @@ from typing import Any, List, Optional
from uuid import UUID from uuid import UUID
from logger import get_logger from logger import get_logger
from models.settings import CommonsDep, common_dependencies
from models.users import User
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.users import User
logger = get_logger(__name__) logger = get_logger(__name__)
@ -79,11 +80,15 @@ class Brain(BaseModel):
response = ( response = (
self.commons["supabase"] self.commons["supabase"]
.from_("brains_users") .from_("brains_users")
.select("id:brain_id, brains (id: brain_id, name)") .select("id:brain_id, rights, brains (id: brain_id, name)")
.filter("user_id", "eq", user_id) .filter("user_id", "eq", user_id)
.execute() .execute()
) )
return [item["brains"] for item in response.data] user_brains = []
for item in response.data:
user_brains.append(item["brains"])
user_brains[-1]["rights"] = item["rights"]
return user_brains
def get_brain_for_user(self, user_id): def get_brain_for_user(self, user_id):
response = ( response = (

View File

@ -0,0 +1,12 @@
from uuid import UUID
from models.settings import common_dependencies
def update_brain_user_rights(brain_id: UUID, user_id: UUID, rights: str) -> None:
commons = common_dependencies()
commons["supabase"].table("brains_users").update({"rights": rights}).eq(
"brain_id",
brain_id,
).eq("user_id", user_id).execute()

View File

@ -1,7 +1,9 @@
from uuid import UUID
from models.settings import common_dependencies from models.settings import common_dependencies
def get_user_email_by_user_id(user_id: int) -> str: def get_user_email_by_user_id(user_id: UUID) -> str:
commons = common_dependencies() commons = common_dependencies()
response = ( response = (
commons["supabase"] commons["supabase"]

View File

@ -0,0 +1,13 @@
from uuid import UUID
from models.settings import common_dependencies
def get_user_id_by_user_email(email: str) -> UUID:
commons = common_dependencies()
response = (
commons["supabase"]
.rpc("get_user_id_by_user_email", {"user_email": email})
.execute()
)
return response.data[0]["user_id"]

View File

@ -3,10 +3,14 @@ from uuid import UUID
from auth import AuthBearer, get_current_user from auth import AuthBearer, get_current_user
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from logger import get_logger from logger import get_logger
from models.brains import (Brain, get_default_user_brain, from models.brains import (
get_default_user_brain_or_create_new) Brain,
get_default_user_brain,
get_default_user_brain_or_create_new,
)
from models.settings import common_dependencies from models.settings import 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
logger = get_logger(__name__) logger = get_logger(__name__)
@ -47,10 +51,7 @@ async def get_default_brain_endpoint(current_user: User = Depends(get_current_us
""" """
brain = get_default_user_brain_or_create_new(current_user) brain = get_default_user_brain_or_create_new(current_user)
return { return {"id": brain.id, "name": brain.name, "rights": "Owner"}
"id": brain.id,
"name": brain.name,
}
# get one brain - Currently not used in FE # get one brain - Currently not used in FE
@ -77,7 +78,6 @@ async def get_brain_endpoint(
return { return {
"id": brain_id, "id": brain_id,
"name": brains[0]["name"], "name": brains[0]["name"],
"status": brains[0]["status"],
} }
else: else:
return HTTPException( return HTTPException(
@ -122,6 +122,7 @@ async def create_brain_endpoint(
return { return {
"id": brain.id, # pyright: ignore reportPrivateUsage=none "id": brain.id, # pyright: ignore reportPrivateUsage=none
"name": brain.name, "name": brain.name,
"rights": "Owner",
} }

View File

@ -1,4 +1,3 @@
import os
from typing import List from typing import List
from uuid import UUID from uuid import UUID
@ -7,12 +6,21 @@ from fastapi import APIRouter, Depends, HTTPException
from models.brains import Brain from models.brains import Brain
from models.brains_subscription_invitations import BrainSubscription from models.brains_subscription_invitations import BrainSubscription
from models.users import User from models.users import User
from repository.brain_subscription.resend_invitation_email import \ from pydantic import BaseModel
resend_invitation_email from repository.brain.update_user_rights import update_brain_user_rights
from repository.brain_subscription.subscription_invitation_service import \ from repository.brain_subscription.resend_invitation_email import (
SubscriptionInvitationService resend_invitation_email,
)
from repository.brain_subscription.subscription_invitation_service import (
SubscriptionInvitationService,
)
from repository.user.get_user_email_by_user_id import get_user_email_by_user_id from repository.user.get_user_email_by_user_id import get_user_email_by_user_id
from routes.authorizations.brain_authorization import has_brain_authorization from repository.user.get_user_id_by_user_email import get_user_id_by_user_email
from routes.authorizations.brain_authorization import (
has_brain_authorization,
validate_brain_authorization,
)
from routes.headers.get_origin_header import get_origin_header from routes.headers.get_origin_header import get_origin_header
subscription_router = APIRouter() subscription_router = APIRouter()
@ -23,14 +31,19 @@ subscription_service = SubscriptionInvitationService()
"/brains/{brain_id}/subscription", "/brains/{brain_id}/subscription",
dependencies=[ dependencies=[
Depends( Depends(
AuthBearer(), AuthBearer(),
), ),
Depends(has_brain_authorization), Depends(has_brain_authorization),
Depends(get_origin_header), Depends(get_origin_header),
], ],
tags=["BrainSubscription"], tags=["BrainSubscription"],
) )
def invite_users_to_brain(brain_id: UUID, users: List[dict], origin: str = Depends(get_origin_header), current_user: User = Depends(get_current_user)): def invite_users_to_brain(
brain_id: UUID,
users: List[dict],
origin: str = Depends(get_origin_header),
current_user: User = Depends(get_current_user),
):
""" """
Invite multiple users to a brain by their emails. This function creates Invite multiple users to a brain by their emails. This function creates
or updates a brain subscription invitation for each user and sends an or updates a brain subscription invitation for each user and sends an
@ -38,11 +51,15 @@ def invite_users_to_brain(brain_id: UUID, users: List[dict], origin: str = Depen
""" """
for user in users: for user in users:
subscription = BrainSubscription(brain_id=brain_id, email=user['email'], rights=user['rights']) subscription = BrainSubscription(
brain_id=brain_id, email=user["email"], rights=user["rights"]
)
try: try:
subscription_service.create_or_update_subscription_invitation(subscription) subscription_service.create_or_update_subscription_invitation(subscription)
resend_invitation_email(subscription, inviter_email=current_user.email or "Quivr", origin=origin) resend_invitation_email(
subscription, inviter_email=current_user.email or "Quivr", origin=origin
)
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"Error inviting user: {e}") raise HTTPException(status_code=400, detail=f"Error inviting user: {e}")
@ -137,7 +154,9 @@ def get_user_invitation(brain_id: UUID, current_user: User = Depends(get_current
"/brains/{brain_id}/subscription/accept", "/brains/{brain_id}/subscription/accept",
tags=["Brain"], tags=["Brain"],
) )
async def accept_invitation(brain_id: UUID, current_user: User = Depends(get_current_user)): async def accept_invitation(
brain_id: UUID, current_user: User = Depends(get_current_user)
):
""" """
Accept an invitation to a brain for a user. This function removes the Accept an invitation to a brain for a user. This function removes the
invitation from the subscription invitations and adds the user to the invitation from the subscription invitations and adds the user to the
@ -159,7 +178,7 @@ async def accept_invitation(brain_id: UUID, current_user: User = Depends(get_cur
try: try:
brain = Brain(id=brain_id) brain = Brain(id=brain_id)
brain.create_brain_user( brain.create_brain_user(
user_id=current_user.id, rights=invitation['rights'], default_brain=False user_id=current_user.id, rights=invitation["rights"], default_brain=False
) )
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"Error adding user to brain: {e}") raise HTTPException(status_code=400, detail=f"Error adding user to brain: {e}")
@ -176,7 +195,9 @@ async def accept_invitation(brain_id: UUID, current_user: User = Depends(get_cur
"/brains/{brain_id}/subscription/decline", "/brains/{brain_id}/subscription/decline",
tags=["Brain"], tags=["Brain"],
) )
async def decline_invitation(brain_id: UUID, current_user: User = Depends(get_current_user)): async def decline_invitation(
brain_id: UUID, current_user: User = Depends(get_current_user)
):
""" """
Decline an invitation to a brain for a user. This function removes the Decline an invitation to a brain for a user. This function removes the
invitation from the subscription invitations. invitation from the subscription invitations.
@ -190,7 +211,7 @@ async def decline_invitation(brain_id: UUID, current_user: User = Depends(get_cu
invitation = subscription_service.fetch_invitation(subscription) invitation = subscription_service.fetch_invitation(subscription)
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"Error fetching invitation: {e}") raise HTTPException(status_code=400, detail=f"Error fetching invitation: {e}")
if not invitation: if not invitation:
raise HTTPException(status_code=404, detail="Invitation not found") raise HTTPException(status_code=404, detail="Invitation not found")
@ -200,3 +221,36 @@ async def decline_invitation(brain_id: UUID, current_user: User = Depends(get_cu
raise HTTPException(status_code=400, detail=f"Error removing invitation: {e}") raise HTTPException(status_code=400, detail=f"Error removing invitation: {e}")
return {"message": "Invitation declined successfully"} return {"message": "Invitation declined successfully"}
class BrainSubscriptionUpdatableProperties(BaseModel):
rights: str | None
email: str
@subscription_router.put("/brains/{brain_id}/subscription")
def update_brain_subscription(
brain_id: UUID,
subscription: BrainSubscriptionUpdatableProperties,
current_user: User = Depends(get_current_user),
):
validate_brain_authorization(brain_id, current_user.id, "Owner")
user_email = subscription.email
if user_email == current_user.email:
raise HTTPException(
status_code=403,
detail="You can't change your own permissions",
)
user_id = get_user_id_by_user_email(user_email)
if subscription.rights is None:
brain = Brain(
id=brain_id,
)
brain.delete_user_from_brain(user_id)
else:
update_brain_user_rights(brain_id, user_id, subscription.rights)
return {"message": "Brain subscription updated successfully"}

View File

@ -97,7 +97,6 @@ def test_retrieve_one_brain(client, api_key):
brain = response.json() brain = response.json()
assert "id" in brain assert "id" in brain
assert "name" in brain assert "name" in brain
assert "status" in brain
def test_delete_all_brains(client, api_key): def test_delete_all_brains(client, api_key):

View File

@ -7,6 +7,7 @@ import { useCallback, useEffect, useState } from "react";
import { useSubscriptionApi } from "@/lib/api/subscription/useSubscriptionApi"; import { useSubscriptionApi } from "@/lib/api/subscription/useSubscriptionApi";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext"; import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useToast } from "@/lib/hooks"; import { useToast } from "@/lib/hooks";
import { useEventTracking } from "@/services/analytics/useEventTracking";
interface UseInvitationReturn { interface UseInvitationReturn {
brainId: UUID | undefined; brainId: UUID | undefined;
@ -20,6 +21,7 @@ export const useInvitation = (): UseInvitationReturn => {
const params = useParams(); const params = useParams();
const brainId = params?.brainId as UUID | undefined; const brainId = params?.brainId as UUID | undefined;
const { publish } = useToast(); const { publish } = useToast();
const { track } = useEventTracking();
if (brainId === undefined) { if (brainId === undefined) {
throw new Error("Brain ID is undefined"); throw new Error("Brain ID is undefined");
} }
@ -32,7 +34,7 @@ export const useInvitation = (): UseInvitationReturn => {
[] []
); );
const { addBrain, setActiveBrain } = useBrainContext(); const { fetchAllBrains, setActiveBrain } = useBrainContext();
const router = useRouter(); const router = useRouter();
// Check invitation on component mount // Check invitation on component mount
useEffect(() => { useEffect(() => {
@ -67,9 +69,9 @@ export const useInvitation = (): UseInvitationReturn => {
// After success, redirect user to a specific page -> chat page // After success, redirect user to a specific page -> chat page
try { try {
const response = await acceptInvitation(brainId); const response = await acceptInvitation(brainId);
console.log(response.message); void track("BRAIN_ADDED");
await addBrain(brainId); await fetchAllBrains();
setActiveBrain({ id: brainId, name: "BrainName" }); setActiveBrain({ id: brainId, name: "BrainName" });
//set brainId as active brain //set brainId as active brain

View File

@ -18,6 +18,7 @@ const axiosPostMock = vi.fn(() => ({
})); }));
const axiosDeleteMock = vi.fn(() => ({})); const axiosDeleteMock = vi.fn(() => ({}));
const axiosPutMock = vi.fn(() => ({}));
vi.mock("@/lib/hooks", () => ({ vi.mock("@/lib/hooks", () => ({
useAxios: vi.fn(() => ({ useAxios: vi.fn(() => ({
@ -25,6 +26,7 @@ vi.mock("@/lib/hooks", () => ({
get: axiosGetMock, get: axiosGetMock,
post: axiosPostMock, post: axiosPostMock,
delete: axiosDeleteMock, delete: axiosDeleteMock,
put: axiosPutMock,
}, },
})), })),
})); }));
@ -144,4 +146,22 @@ describe("useBrainApi", () => {
expect(axiosGetMock).toHaveBeenCalledTimes(1); expect(axiosGetMock).toHaveBeenCalledTimes(1);
expect(axiosGetMock).toHaveBeenCalledWith(`/brains/${id}/users`); expect(axiosGetMock).toHaveBeenCalledWith(`/brains/${id}/users`);
}); });
it("should call updateBrainAccess with the correct parameters", async () => {
const {
result: {
current: { updateBrainAccess },
},
} = renderHook(() => useBrainApi());
const brainId = "123";
const email = "456";
const subscription = {
rights: "viewer",
};
await updateBrainAccess(brainId, email, subscription);
expect(axiosPutMock).toHaveBeenCalledTimes(1);
expect(axiosPutMock).toHaveBeenCalledWith(
`/brains/${brainId}/subscription`,
{ ...subscription, email }
);
});
}); });

View File

@ -1,7 +1,7 @@
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 } from "@/lib/context/BrainProvider/types"; import { Brain, MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
import { Document } from "@/lib/types/Document"; import { Document } from "@/lib/types/Document";
export const getBrainDocuments = async ( export const getBrainDocuments = async (
@ -18,9 +18,10 @@ export const getBrainDocuments = async (
export const createBrain = async ( export const createBrain = async (
name: string, name: string,
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
): Promise<Brain> => { ): Promise<MinimalBrainForUser> => {
const createdBrain = (await axiosInstance.post<Brain>(`/brains/`, { name })) const createdBrain = (
.data; await axiosInstance.post<MinimalBrainForUser>(`/brains/`, { name })
).data;
return createdBrain; return createdBrain;
}; };
@ -45,18 +46,17 @@ export const deleteBrain = async (
export const getDefaultBrain = async ( export const getDefaultBrain = async (
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
): Promise<Brain | undefined> => { ): Promise<MinimalBrainForUser | undefined> => {
const defaultBrain = (await axiosInstance.get<Brain>(`/brains/default/`)) return (await axiosInstance.get<MinimalBrainForUser>(`/brains/default/`))
.data; .data;
return { id: defaultBrain.id, name: defaultBrain.name };
}; };
export const getBrains = async ( export const getBrains = async (
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
): Promise<Brain[]> => { ): Promise<MinimalBrainForUser[]> => {
const brains = (await axiosInstance.get<{ brains: Brain[] }>(`/brains/`)) const brains = (
.data; await axiosInstance.get<{ brains: MinimalBrainForUser[] }>(`/brains/`)
).data;
return brains.brains; return brains.brains;
}; };
@ -78,3 +78,19 @@ export const getBrainUsers = async (
return (await axiosInstance.get<Subscription[]>(`/brains/${brainId}/users`)) return (await axiosInstance.get<Subscription[]>(`/brains/${brainId}/users`))
.data; .data;
}; };
export type SubscriptionUpdatableProperties = {
rights: BrainRoleType | null;
};
export const updateBrainAccess = async (
brainId: string,
userEmail: string,
subscription: SubscriptionUpdatableProperties,
axiosInstance: AxiosInstance
): Promise<void> => {
await axiosInstance.put(`/brains/${brainId}/subscription`, {
...subscription,
email: userEmail,
});
};

View File

@ -10,6 +10,8 @@ import {
getBrainUsers, getBrainUsers,
getDefaultBrain, getDefaultBrain,
Subscription, Subscription,
SubscriptionUpdatableProperties,
updateBrainAccess,
} from "./brain"; } from "./brain";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@ -30,5 +32,10 @@ export const useBrainApi = () => {
) => addBrainSubscriptions(brainId, subscriptions, axiosInstance), ) => addBrainSubscriptions(brainId, subscriptions, axiosInstance),
getBrainUsers: async (brainId: string) => getBrainUsers: async (brainId: string) =>
getBrainUsers(brainId, axiosInstance), getBrainUsers(brainId, axiosInstance),
updateBrainAccess: async (
brainId: string,
userEmail: string,
subscription: SubscriptionUpdatableProperties
) => updateBrainAccess(brainId, userEmail, subscription, axiosInstance),
}; };
}; };

View File

@ -73,7 +73,7 @@ export const BrainsDropDown = (): JSX.Element => {
</span> </span>
<span className="flex-1">{brain.name}</span> <span className="flex-1">{brain.name}</span>
</button> </button>
<BrainActions brainId={brain.id} /> <BrainActions brain={brain} />
</div> </div>
); );
} }

View File

@ -1,16 +1,16 @@
import { UUID } from "crypto"; import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
import { DeleteBrain, ShareBrain } from "./components"; import { DeleteBrain, ShareBrain } from "./components";
type BrainActionsProps = { type BrainActionsProps = {
brainId: UUID; brain: MinimalBrainForUser;
}; };
export const BrainActions = ({ brainId }: 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">
<ShareBrain brainId={brainId} /> {brain.rights === "Owner" && <ShareBrain brainId={brain.id} />}
<DeleteBrain brainId={brainId} /> <DeleteBrain brainId={brain.id} />
</div> </div>
); );
}; };

View File

@ -29,6 +29,8 @@ export const ShareBrain = ({ brainId }: ShareBrainModalProps): JSX.Element => {
setIsShareModalOpen, setIsShareModalOpen,
isShareModalOpen, isShareModalOpen,
brainUsers, brainUsers,
fetchBrainUsers,
isFetchingBrainUsers,
} = useShareBrain(brainId); } = useShareBrain(brainId);
const canAddNewRow = const canAddNewRow =
@ -103,13 +105,19 @@ export const ShareBrain = ({ brainId }: ShareBrainModalProps): JSX.Element => {
</form> </form>
<div className="bg-gray-100 h-0.5 mb-5 border-gray-200 dark:border-gray-700" /> <div className="bg-gray-100 h-0.5 mb-5 border-gray-200 dark:border-gray-700" />
<p className="text-lg font-bold">Users with access</p> <p className="text-lg font-bold">Users with access</p>
{brainUsers.map((subscription) => ( {isFetchingBrainUsers ? (
<BrainUser <p className="text-gray-500">Loading...</p>
key={subscription.email} ) : (
email={subscription.email} brainUsers.map((subscription) => (
rights={subscription.rights} <BrainUser
/> key={subscription.email}
))} email={subscription.email}
rights={subscription.rights}
brainId={brainId}
fetchBrainUsers={fetchBrainUsers}
/>
))
)}
</Modal> </Modal>
); );
}; };

View File

@ -1,8 +1,10 @@
import { useState } from "react"; import { useState } from "react";
import { MdOutlineRemoveCircle } from "react-icons/md"; import { MdOutlineRemoveCircle } from "react-icons/md";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import Field from "@/lib/components/ui/Field"; import Field from "@/lib/components/ui/Field";
import { Select } from "@/lib/components/ui/Select"; import { Select } from "@/lib/components/ui/Select";
import { useToast } from "@/lib/hooks";
import { BrainRoleType } from "../../../types"; import { BrainRoleType } from "../../../types";
import { availableRoles } from "../types"; import { availableRoles } from "../types";
@ -10,20 +12,35 @@ import { availableRoles } from "../types";
type BrainUserProps = { type BrainUserProps = {
email: string; email: string;
rights: BrainRoleType; rights: BrainRoleType;
brainId: string;
fetchBrainUsers: () => Promise<void>;
}; };
export const BrainUser = ({ export const BrainUser = ({
email, email,
rights: role, rights: role,
brainId,
fetchBrainUsers,
}: BrainUserProps): JSX.Element => { }: BrainUserProps): JSX.Element => {
const { updateBrainAccess } = useBrainApi();
const { publish } = useToast();
const [selectedRole, setSelectedRole] = useState<BrainRoleType>(role); const [selectedRole, setSelectedRole] = useState<BrainRoleType>(role);
const updateSelectedRole = (newRole: BrainRoleType) => { const updateSelectedRole = async (newRole: BrainRoleType) => {
setSelectedRole(newRole); setSelectedRole(newRole);
await updateBrainAccess(brainId, email, {
rights: newRole,
});
publish({ variant: "success", text: `Updated ${email} to ${newRole}` });
void fetchBrainUsers();
}; };
const removeCurrentInvitation = () => { const removeCurrentInvitation = async () => {
alert("soon"); await updateBrainAccess(brainId, email, {
rights: null,
});
publish({ variant: "success", text: `Removed ${email} from brain` });
void fetchBrainUsers();
}; };
return ( return (
@ -31,7 +48,10 @@ export const BrainUser = ({
data-testid="assignation-row" data-testid="assignation-row"
className="flex flex-row align-center my-2 gap-3 items-center" className="flex flex-row align-center my-2 gap-3 items-center"
> >
<div className="cursor-pointer" onClick={removeCurrentInvitation}> <div
className="cursor-pointer"
onClick={() => void removeCurrentInvitation()}
>
<MdOutlineRemoveCircle /> <MdOutlineRemoveCircle />
</div> </div>
<div className="flex flex-1"> <div className="flex flex-1">
@ -46,7 +66,7 @@ export const BrainUser = ({
/> />
</div> </div>
<Select <Select
onChange={updateSelectedRole} onChange={(newRole) => void updateSelectedRole(newRole)}
value={selectedRole} value={selectedRole}
options={availableRoles} options={availableRoles}
/> />

View File

@ -12,6 +12,7 @@ import { generateBrainAssignation } from "../utils/generateBrainAssignation";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useShareBrain = (brainId: string) => { export const useShareBrain = (brainId: string) => {
const [brainUsers, setBrainUsers] = useState<Subscription[]>([]); const [brainUsers, setBrainUsers] = useState<Subscription[]>([]);
const [isFetchingBrainUsers, setFetchingBrainUsers] = useState(false);
const [sendingInvitation, setSendingInvitation] = useState(false); const [sendingInvitation, setSendingInvitation] = useState(false);
const [isShareModalOpen, setIsShareModalOpen] = useState(false); const [isShareModalOpen, setIsShareModalOpen] = useState(false);
const [roleAssignations, setRoleAssignation] = useState< const [roleAssignations, setRoleAssignation] = useState<
@ -93,12 +94,23 @@ export const useShareBrain = (brainId: string) => {
setRoleAssignation([...roleAssignations, generateBrainAssignation()]); setRoleAssignation([...roleAssignations, generateBrainAssignation()]);
}; };
useEffect(() => { const fetchBrainUsers = async () => {
const fetchBrainUsers = async () => { // Optimistic update
setFetchingBrainUsers(brainUsers.length === 0);
try {
const users = await getBrainUsers(brainId); const users = await getBrainUsers(brainId);
setBrainUsers(users.filter(({ email }) => email !== session?.user.email)); setBrainUsers(users.filter(({ email }) => email !== session?.user.email));
}; } catch {
publish({
variant: "danger",
text: "An error occurred while fetching brain users",
});
} finally {
setFetchingBrainUsers(false);
}
};
useEffect(() => {
void fetchBrainUsers(); void fetchBrainUsers();
}, []); }, []);
@ -114,5 +126,7 @@ export const useShareBrain = (brainId: string) => {
setIsShareModalOpen, setIsShareModalOpen,
isShareModalOpen, isShareModalOpen,
brainUsers, brainUsers,
fetchBrainUsers,
isFetchingBrainUsers,
}; };
}; };

View File

@ -5,6 +5,7 @@ export type SelectOptionsProps = {
value: BrainRoleType; value: BrainRoleType;
}; };
export const availableRoles: SelectOptionsProps[] = [ export const availableRoles: SelectOptionsProps[] = [
{ label: "Viewer", value: "viewer" }, { label: "Viewer", value: "Viewer" },
{ label: "Editor", value: "editor" }, { label: "Editor", value: "Editor" },
{ label: "Owner", value: "Owner" },
]; ];

View File

@ -10,7 +10,7 @@ import {
getBrainFromLocalStorage, getBrainFromLocalStorage,
saveBrainInLocalStorage, saveBrainInLocalStorage,
} from "../helpers/brainLocalStorage"; } from "../helpers/brainLocalStorage";
import { Brain } from "../types"; import { MinimalBrainForUser } from "../types";
// CAUTION: This hook should be use in BrainProvider only. You may be need `useBrainContext` instead. // CAUTION: This hook should be use in BrainProvider only. You may be need `useBrainContext` instead.
@ -18,10 +18,10 @@ import { Brain } from "../types";
export const useBrainProvider = () => { export const useBrainProvider = () => {
const { publish } = useToast(); const { publish } = useToast();
const { track } = useEventTracking(); const { track } = useEventTracking();
const { createBrain, deleteBrain, getBrains, getDefaultBrain, getBrain } = const { createBrain, deleteBrain, getBrains, getDefaultBrain } =
useBrainApi(); useBrainApi();
const [allBrains, setAllBrains] = useState<Brain[]>([]); const [allBrains, setAllBrains] = useState<MinimalBrainForUser[]>([]);
const [currentBrainId, setCurrentBrainId] = useState<null | UUID>(null); const [currentBrainId, setCurrentBrainId] = useState<null | UUID>(null);
const [isFetchingBrains, setIsFetchingBrains] = useState(false); const [isFetchingBrains, setIsFetchingBrains] = useState(false);
@ -45,28 +45,6 @@ export const useBrainProvider = () => {
} }
}; };
const addBrain = async (id: UUID): Promise<void> => {
const brain = await getBrain(id);
if (brain === undefined) {
publish({
variant: "danger",
text: "Error occurred while adding a brain",
});
return;
}
try {
setAllBrains((prevBrains) => [...prevBrains, brain]);
saveBrainInLocalStorage(brain);
void track("BRAIN_ADDED");
} catch {
publish({
variant: "danger",
text: "Error occurred while adding a brain",
});
}
};
const deleteBrainHandler = async (id: UUID) => { const deleteBrainHandler = async (id: UUID) => {
await deleteBrain(id); await deleteBrain(id);
setAllBrains((prevBrains) => prevBrains.filter((brain) => brain.id !== id)); setAllBrains((prevBrains) => prevBrains.filter((brain) => brain.id !== id));
@ -124,7 +102,6 @@ export const useBrainProvider = () => {
allBrains, allBrains,
createBrain: createBrainHandler, createBrain: createBrainHandler,
deleteBrain: deleteBrainHandler, deleteBrain: deleteBrainHandler,
addBrain,
setActiveBrain, setActiveBrain,
fetchAllBrains, fetchAllBrains,
setDefaultBrain, setDefaultBrain,

View File

@ -1,5 +1,6 @@
import { UUID } from "crypto"; import { UUID } from "crypto";
import { BrainRoleType } from "@/lib/components/NavBar/components/NavItems/components/BrainsDropDown/components/BrainActions/types";
import { Document } from "@/lib/types/Document"; import { Document } from "@/lib/types/Document";
import { useBrainProvider } from "./hooks/useBrainProvider"; import { useBrainProvider } from "./hooks/useBrainProvider";
@ -14,4 +15,10 @@ export type Brain = {
temperature?: string; temperature?: string;
}; };
export type MinimalBrainForUser = {
id: UUID;
name: string;
rights: BrainRoleType;
};
export type BrainContextType = ReturnType<typeof useBrainProvider>; export type BrainContextType = ReturnType<typeof useBrainProvider>;

View File

@ -0,0 +1,17 @@
CREATE OR REPLACE FUNCTION public.get_user_id_by_user_email(user_email text)
RETURNS TABLE (user_id uuid)
SECURITY DEFINER
AS $$
BEGIN
RETURN QUERY SELECT au.id::uuid FROM auth.users au WHERE au.email = user_email;
END;
$$ LANGUAGE plpgsql;
-- Update migrations table
INSERT INTO migrations (name)
SELECT '20230717173000_add_get_user_id_by_user_email'
WHERE NOT EXISTS (
SELECT 1 FROM migrations WHERE name = '20230717173000_add_get_user_id_by_user_email'
);
COMMIT;

View File

@ -175,13 +175,25 @@ BEGIN
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION public.get_user_id_by_user_email(user_email text)
RETURNS TABLE (user_id uuid)
SECURITY DEFINER
AS $$
BEGIN
RETURN QUERY SELECT au.id::uuid FROM auth.users au WHERE au.email = user_email;
END;
$$ LANGUAGE plpgsql;
CREATE TABLE IF NOT EXISTS migrations ( CREATE TABLE IF NOT EXISTS migrations (
name VARCHAR(255) PRIMARY KEY, name VARCHAR(255) PRIMARY KEY,
executed_at TIMESTAMPTZ DEFAULT current_timestamp executed_at TIMESTAMPTZ DEFAULT current_timestamp
); );
INSERT INTO migrations (name) INSERT INTO migrations (name)
SELECT '20230717164900_add_get_user_email_by_user_id' SELECT '20230717173000_add_get_user_id_by_user_email'
WHERE NOT EXISTS ( WHERE NOT EXISTS (
SELECT 1 FROM migrations WHERE name = '20230717164900_add_get_user_email_by_user_id' SELECT 1 FROM migrations WHERE name = '20230717173000_add_get_user_id_by_user_email'
); );