mirror of
https://github.com/StanGirard/quivr.git
synced 2024-09-11 13:15:41 +03:00
Shareable brain 8 (#674)
* feat(ShareableBrain): add get brain users endpoints * feat(sdk): add getBrainUsers * feat(ShareableBrain): display users with access * feat: rename role to rights * fix(Brain): fecth brains on auth status change
This commit is contained in:
parent
4d00a1ec92
commit
430ab54479
11
backend/core/repository/user/get_user_email_by_user_id.py
Normal file
11
backend/core/repository/user/get_user_email_by_user_id.py
Normal file
@ -0,0 +1,11 @@
|
||||
from models.settings import common_dependencies
|
||||
|
||||
|
||||
def get_user_email_by_user_id(user_id: int) -> str:
|
||||
commons = common_dependencies()
|
||||
response = (
|
||||
commons["supabase"]
|
||||
.rpc("get_user_email_by_user_id", {"user_id": user_id})
|
||||
.execute()
|
||||
)
|
||||
return response.data[0]["email"]
|
@ -1,11 +1,16 @@
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from auth.auth_bearer import get_current_user
|
||||
from auth.auth_bearer import AuthBearer, get_current_user
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from models.brains import Brain
|
||||
from models.brains_subscription_invitations import BrainSubscription
|
||||
from models.users import User
|
||||
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,
|
||||
)
|
||||
|
||||
subscription_router = APIRouter()
|
||||
|
||||
@ -33,6 +38,33 @@ async def invite_user_to_brain(
|
||||
return {"message": "Invitations sent successfully"}
|
||||
|
||||
|
||||
@subscription_router.get(
|
||||
"/brain/{brain_id}/users",
|
||||
dependencies=[Depends(AuthBearer()), Depends(has_brain_authorization())],
|
||||
)
|
||||
def get_brain_users(
|
||||
brain_id: UUID,
|
||||
):
|
||||
"""
|
||||
Get all users for a brain
|
||||
"""
|
||||
brain = Brain(
|
||||
id=brain_id,
|
||||
)
|
||||
brain_users = brain.get_brain_users()
|
||||
|
||||
brain_access_list = []
|
||||
|
||||
for brain_user in brain_users:
|
||||
brain_access = {}
|
||||
# TODO: find a way to fetch user email concurrently
|
||||
brain_access["email"] = get_user_email_by_user_id(brain_user["user_id"])
|
||||
brain_access["rights"] = brain_user["rights"]
|
||||
brain_access_list.append(brain_access)
|
||||
|
||||
return brain_access_list
|
||||
|
||||
|
||||
@subscription_router.delete(
|
||||
"/brain/{brain_id}/subscription",
|
||||
)
|
||||
@ -55,10 +87,10 @@ async def remove_user_subscription(
|
||||
if user_brain.get("rights") != "Owner":
|
||||
brain.delete_user_from_brain(current_user.id)
|
||||
else:
|
||||
brain_other_users = brain.get_brain_users()
|
||||
brain_users = brain.get_brain_users()
|
||||
brain_other_owners = [
|
||||
brain
|
||||
for brain in brain_other_users
|
||||
for brain in brain_users
|
||||
if brain["rights"] == "Owner"
|
||||
and str(brain["user_id"]) != str(current_user.id)
|
||||
]
|
||||
|
@ -6,15 +6,17 @@ import Footer from "@/lib/components/Footer";
|
||||
import { NavBar } from "@/lib/components/NavBar";
|
||||
import { TrackingWrapper } from "@/lib/components/TrackingWrapper";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
|
||||
// This wrapper is used to make effect calls at a high level in app rendering.
|
||||
export const App = ({ children }: PropsWithChildren): JSX.Element => {
|
||||
const { fetchAllBrains, fetchAndSetActiveBrain } = useBrainContext();
|
||||
const { session } = useSupabase();
|
||||
|
||||
useEffect(() => {
|
||||
void fetchAllBrains();
|
||||
void fetchAndSetActiveBrain();
|
||||
}, []);
|
||||
}, [session?.user]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -131,4 +131,17 @@ describe("useBrainApi", () => {
|
||||
subscriptions
|
||||
);
|
||||
});
|
||||
|
||||
it("should call getBrainUsers with the correct parameters", async () => {
|
||||
const {
|
||||
result: {
|
||||
current: { getBrainUsers },
|
||||
},
|
||||
} = renderHook(() => useBrainApi());
|
||||
const id = "123";
|
||||
await getBrainUsers(id);
|
||||
|
||||
expect(axiosGetMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosGetMock).toHaveBeenCalledWith(`/brain/${id}/users`);
|
||||
});
|
||||
});
|
||||
|
@ -61,12 +61,20 @@ export const getBrains = async (
|
||||
return brains.brains;
|
||||
};
|
||||
|
||||
export type Subscription = { email: string; rights: BrainRoleType }[];
|
||||
export type Subscription = { email: string; rights: BrainRoleType };
|
||||
|
||||
export const addBrainSubscriptions = async (
|
||||
brainId: string,
|
||||
subscriptions: Subscription,
|
||||
subscriptions: Subscription[],
|
||||
axiosInstance: AxiosInstance
|
||||
): Promise<void> => {
|
||||
await axiosInstance.post(`/brain/${brainId}/subscription`, subscriptions);
|
||||
};
|
||||
|
||||
export const getBrainUsers = async (
|
||||
brainId: string,
|
||||
axiosInstance: AxiosInstance
|
||||
): Promise<Subscription[]> => {
|
||||
return (await axiosInstance.get<Subscription[]>(`/brain/${brainId}/users`))
|
||||
.data;
|
||||
};
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
getBrain,
|
||||
getBrainDocuments,
|
||||
getBrains,
|
||||
getBrainUsers,
|
||||
getDefaultBrain,
|
||||
Subscription,
|
||||
} from "./brain";
|
||||
@ -25,7 +26,9 @@ export const useBrainApi = () => {
|
||||
getBrain: async (id: string) => getBrain(id, axiosInstance),
|
||||
addBrainSubscriptions: async (
|
||||
brainId: string,
|
||||
subscriptions: Subscription
|
||||
subscriptions: Subscription[]
|
||||
) => addBrainSubscriptions(brainId, subscriptions, axiosInstance),
|
||||
getBrainUsers: async (brainId: string) =>
|
||||
getBrainUsers(brainId, axiosInstance),
|
||||
};
|
||||
};
|
||||
|
@ -8,7 +8,8 @@ import { MdContentPaste, MdShare } from "react-icons/md";
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { Modal } from "@/lib/components/ui/Modal";
|
||||
|
||||
import { InvitedUserRow } from "./components/InvitedUserRow";
|
||||
import { BrainUser } from "./components";
|
||||
import { UserToInvite } from "./components/UserToInvite";
|
||||
import { useShareBrain } from "./hooks/useShareBrain";
|
||||
|
||||
type ShareBrainModalProps = {
|
||||
@ -27,6 +28,7 @@ export const ShareBrain = ({ brainId }: ShareBrainModalProps): JSX.Element => {
|
||||
sendingInvitation,
|
||||
setIsShareModalOpen,
|
||||
isShareModalOpen,
|
||||
brainUsers,
|
||||
} = useShareBrain(brainId);
|
||||
|
||||
const canAddNewRow =
|
||||
@ -75,7 +77,7 @@ export const ShareBrain = ({ brainId }: ShareBrainModalProps): JSX.Element => {
|
||||
<div className="bg-gray-100 h-0.5 mb-5 border-gray-200 dark:border-gray-700" />
|
||||
|
||||
{roleAssignations.map((roleAssignation, index) => (
|
||||
<InvitedUserRow
|
||||
<UserToInvite
|
||||
key={roleAssignation.id}
|
||||
onChange={updateRoleAssignation(index)}
|
||||
removeCurrentInvitation={removeRoleAssignation(index)}
|
||||
@ -99,6 +101,15 @@ export const ShareBrain = ({ brainId }: ShareBrainModalProps): JSX.Element => {
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<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>
|
||||
{brainUsers.map((subscription) => (
|
||||
<BrainUser
|
||||
key={subscription.email}
|
||||
email={subscription.email}
|
||||
rights={subscription.rights}
|
||||
/>
|
||||
))}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,20 @@ vi.mock("@/lib/context/BrainConfigProvider/brain-config-provider", () => ({
|
||||
BrainConfigContext: BrainConfigContextMock,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api/brain/useBrainApi", async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import("@/lib/api/brain/useBrainApi")
|
||||
>("@/lib/api/brain/useBrainApi");
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useBrainApi: () => ({
|
||||
...actual.useBrainApi(),
|
||||
getBrainUsers: () => Promise.resolve([]),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe("ShareBrain", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
|
@ -0,0 +1,55 @@
|
||||
import { useState } from "react";
|
||||
import { MdOutlineRemoveCircle } from "react-icons/md";
|
||||
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
import { Select } from "@/lib/components/ui/Select";
|
||||
|
||||
import { BrainRoleType } from "../../../types";
|
||||
import { availableRoles } from "../types";
|
||||
|
||||
type BrainUserProps = {
|
||||
email: string;
|
||||
rights: BrainRoleType;
|
||||
};
|
||||
|
||||
export const BrainUser = ({
|
||||
email,
|
||||
rights: role,
|
||||
}: BrainUserProps): JSX.Element => {
|
||||
const [selectedRole, setSelectedRole] = useState<BrainRoleType>(role);
|
||||
|
||||
const updateSelectedRole = (newRole: BrainRoleType) => {
|
||||
setSelectedRole(newRole);
|
||||
};
|
||||
|
||||
const removeCurrentInvitation = () => {
|
||||
alert("soon");
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="assignation-row"
|
||||
className="flex flex-row align-center my-2 gap-3 items-center"
|
||||
>
|
||||
<div className="cursor-pointer" onClick={removeCurrentInvitation}>
|
||||
<MdOutlineRemoveCircle />
|
||||
</div>
|
||||
<div className="flex flex-1">
|
||||
<Field
|
||||
name="email"
|
||||
required
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
data-testid="role-assignation-email-input"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
onChange={updateSelectedRole}
|
||||
value={selectedRole}
|
||||
options={availableRoles}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -5,29 +5,21 @@ import Field from "@/lib/components/ui/Field";
|
||||
import { Select } from "@/lib/components/ui/Select";
|
||||
|
||||
import { BrainRoleAssignation, BrainRoleType } from "../../../types";
|
||||
import { availableRoles } from "../types";
|
||||
|
||||
type AddUserRowProps = {
|
||||
type UserToInviteProps = {
|
||||
onChange: (newRole: BrainRoleAssignation) => void;
|
||||
removeCurrentInvitation?: () => void;
|
||||
roleAssignation: BrainRoleAssignation;
|
||||
};
|
||||
|
||||
type SelectOptionsProps = {
|
||||
label: string;
|
||||
value: BrainRoleType;
|
||||
};
|
||||
const SelectOptions: SelectOptionsProps[] = [
|
||||
{ label: "Viewer", value: "viewer" },
|
||||
{ label: "Editor", value: "editor" },
|
||||
];
|
||||
|
||||
export const InvitedUserRow = ({
|
||||
export const UserToInvite = ({
|
||||
onChange,
|
||||
removeCurrentInvitation,
|
||||
roleAssignation,
|
||||
}: AddUserRowProps): JSX.Element => {
|
||||
}: UserToInviteProps): JSX.Element => {
|
||||
const [selectedRole, setSelectedRole] = useState<BrainRoleType>(
|
||||
roleAssignation.role
|
||||
roleAssignation.rights
|
||||
);
|
||||
const [email, setEmail] = useState(roleAssignation.email);
|
||||
|
||||
@ -35,7 +27,7 @@ export const InvitedUserRow = ({
|
||||
onChange({
|
||||
...roleAssignation,
|
||||
email,
|
||||
role: selectedRole,
|
||||
rights: selectedRole,
|
||||
});
|
||||
}, [email, selectedRole]);
|
||||
|
||||
@ -62,7 +54,7 @@ export const InvitedUserRow = ({
|
||||
<Select
|
||||
onChange={setSelectedRole}
|
||||
value={selectedRole}
|
||||
options={SelectOptions}
|
||||
options={availableRoles}
|
||||
/>
|
||||
</div>
|
||||
);
|
@ -1 +1,2 @@
|
||||
export * from "./InvitedUserRow";
|
||||
export * from "./BrainUser";
|
||||
export * from "./UserToInvite";
|
||||
|
@ -1,7 +1,9 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Subscription } from "@/lib/api/brain/brain";
|
||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
|
||||
import { BrainRoleAssignation } from "../../../types";
|
||||
@ -9,17 +11,20 @@ import { generateBrainAssignation } from "../utils/generateBrainAssignation";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useShareBrain = (brainId: string) => {
|
||||
const baseUrl = window.location.origin;
|
||||
const { publish } = useToast();
|
||||
const { addBrainSubscriptions } = useBrainApi();
|
||||
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
|
||||
|
||||
const brainShareLink = `${baseUrl}/brain_subscription_invitation=${brainId}`;
|
||||
const [brainUsers, setBrainUsers] = useState<Subscription[]>([]);
|
||||
const [sendingInvitation, setSendingInvitation] = useState(false);
|
||||
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
|
||||
const [roleAssignations, setRoleAssignation] = useState<
|
||||
BrainRoleAssignation[]
|
||||
>([generateBrainAssignation()]);
|
||||
|
||||
const baseUrl = window.location.origin;
|
||||
const brainShareLink = `${baseUrl}/brain_subscription_invitation=${brainId}`;
|
||||
|
||||
const { publish } = useToast();
|
||||
const { addBrainSubscriptions, getBrainUsers } = useBrainApi();
|
||||
const { session } = useSupabase();
|
||||
|
||||
const handleCopyInvitationLink = async () => {
|
||||
await navigator.clipboard.writeText(brainShareLink);
|
||||
publish({
|
||||
@ -63,7 +68,7 @@ export const useShareBrain = (brainId: string) => {
|
||||
.filter(({ email }) => email !== "")
|
||||
.map((assignation) => ({
|
||||
email: assignation.email,
|
||||
rights: assignation.role,
|
||||
rights: assignation.rights,
|
||||
}));
|
||||
|
||||
await addBrainSubscriptions(brainId, inviteUsersPayload);
|
||||
@ -88,6 +93,15 @@ export const useShareBrain = (brainId: string) => {
|
||||
setRoleAssignation([...roleAssignations, generateBrainAssignation()]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBrainUsers = async () => {
|
||||
const users = await getBrainUsers(brainId);
|
||||
setBrainUsers(users.filter(({ email }) => email !== session?.user.email));
|
||||
};
|
||||
|
||||
void fetchBrainUsers();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
roleAssignations,
|
||||
brainShareLink,
|
||||
@ -99,5 +113,6 @@ export const useShareBrain = (brainId: string) => {
|
||||
sendingInvitation,
|
||||
setIsShareModalOpen,
|
||||
isShareModalOpen,
|
||||
brainUsers,
|
||||
};
|
||||
};
|
||||
|
@ -0,0 +1,10 @@
|
||||
import { BrainRoleType } from "../../../types";
|
||||
|
||||
export type SelectOptionsProps = {
|
||||
label: string;
|
||||
value: BrainRoleType;
|
||||
};
|
||||
export const availableRoles: SelectOptionsProps[] = [
|
||||
{ label: "Viewer", value: "viewer" },
|
||||
{ label: "Editor", value: "editor" },
|
||||
];
|
@ -3,7 +3,7 @@ import { BrainRoleAssignation } from "../../../types";
|
||||
export const generateBrainAssignation = (): BrainRoleAssignation => {
|
||||
return {
|
||||
email: "",
|
||||
role: "viewer",
|
||||
rights: "viewer",
|
||||
id: Math.random().toString(),
|
||||
};
|
||||
};
|
||||
|
@ -4,6 +4,6 @@ export type BrainRoleType = (typeof roles)[number];
|
||||
|
||||
export type BrainRoleAssignation = {
|
||||
email: string;
|
||||
role: BrainRoleType;
|
||||
rights: BrainRoleType;
|
||||
id: string;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user