mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-09-11 14:36:35 +03:00
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:
parent
260e20d401
commit
81b57c504a
@ -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 = (
|
||||||
|
12
backend/core/repository/brain/update_user_rights.py
Normal file
12
backend/core/repository/brain/update_user_rights.py
Normal 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()
|
@ -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"]
|
||||||
|
13
backend/core/repository/user/get_user_id_by_user_email.py
Normal file
13
backend/core/repository/user/get_user_id_by_user_email.py
Normal 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"]
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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"}
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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 }
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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" },
|
||||||
];
|
];
|
||||||
|
@ -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,
|
||||||
|
@ -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>;
|
||||||
|
17
scripts/20230717173000_add_get_user_id_by_user_email.sql
Normal file
17
scripts/20230717173000_add_get_user_id_by_user_email.sql
Normal 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;
|
@ -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'
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user