diff --git a/backend/celery_worker.py b/backend/celery_worker.py index 4769d96ab45f..c6ef9eba2502 100644 --- a/backend/celery_worker.py +++ b/backend/celery_worker.py @@ -10,6 +10,9 @@ from models.notifications import NotificationsStatusEnum from models.settings import get_supabase_client from parsers.github import process_github +from repository.brain.update_brain_last_update_time import ( + update_brain_last_update_time, +) from repository.notification.update_notification import update_notification_by_id from utils.processors import filter_file @@ -98,6 +101,8 @@ def process_file_and_notify( message=str(notification_message), ), ) + update_brain_last_update_time(brain_id) + return True @@ -158,4 +163,5 @@ def process_crawl_and_notify( message=str(notification_message), ), ) + update_brain_last_update_time(brain_id) return True diff --git a/backend/models/brain_entity.py b/backend/models/brain_entity.py index a101af601076..528fe0ac836c 100644 --- a/backend/models/brain_entity.py +++ b/backend/models/brain_entity.py @@ -15,6 +15,7 @@ class BrainEntity(BaseModel): openai_api_key: Optional[str] status: Optional[str] prompt_id: Optional[UUID] + last_update: str @property def id(self) -> UUID: @@ -40,3 +41,4 @@ class PublicBrain(BaseModel): name: str description: Optional[str] number_of_subscribers: int = 0 + last_update: str diff --git a/backend/models/databases/supabase/brains.py b/backend/models/databases/supabase/brains.py index 5389d0848dff..de9ea8f80393 100644 --- a/backend/models/databases/supabase/brains.py +++ b/backend/models/databases/supabase/brains.py @@ -78,7 +78,7 @@ def get_user_brains(self, user_id) -> list[MinimalBrainEntity]: def get_public_brains(self) -> list[PublicBrain]: response = ( self.db.from_("brains") - .select("id:brain_id, name, description") + .select("id:brain_id, name, description, last_update") .filter("status", "eq", "public") .execute() ) @@ -88,11 +88,17 @@ def get_public_brains(self) -> list[PublicBrain]: id=item["id"], name=item["name"], description=item["description"], + last_update=item["last_update"], ) brain.number_of_subscribers = self.get_brain_subscribers_count(brain.id) public_brains.append(brain) return public_brains + def update_brain_last_update_time(self, brain_id: UUID) -> None: + self.db.table("brains").update({"last_update": "now()"}).match( + {"brain_id": brain_id} + ).execute() + def get_brain_for_user(self, user_id, brain_id) -> MinimalBrainEntity | None: response = ( self.db.from_("brains_users") diff --git a/backend/repository/brain/update_brain.py b/backend/repository/brain/update_brain.py index 89722c8002c0..467d59ad803f 100644 --- a/backend/repository/brain/update_brain.py +++ b/backend/repository/brain/update_brain.py @@ -3,9 +3,16 @@ from models import BrainEntity, get_supabase_db from models.databases.supabase.brains import BrainUpdatableProperties +from repository.brain.update_brain_last_update_time import update_brain_last_update_time + def update_brain_by_id(brain_id: UUID, brain: BrainUpdatableProperties) -> BrainEntity: """Update a prompt by id""" supabase_db = get_supabase_db() - return supabase_db.update_brain_by_id(brain_id, brain) # type: ignore + brain_update_answer = supabase_db.update_brain_by_id(brain_id, brain) + if brain_update_answer is None: + raise Exception("Brain not found") + + update_brain_last_update_time(brain_id) + return brain_update_answer diff --git a/backend/repository/brain/update_brain_last_update_time.py b/backend/repository/brain/update_brain_last_update_time.py new file mode 100644 index 000000000000..98945e3126fd --- /dev/null +++ b/backend/repository/brain/update_brain_last_update_time.py @@ -0,0 +1,8 @@ +from uuid import UUID + +from models.settings import get_supabase_db + + +def update_brain_last_update_time(brain_id: UUID): + supabase_db = get_supabase_db() + supabase_db.update_brain_last_update_time(brain_id) diff --git a/backend/routes/authorizations/brain_authorization.py b/backend/routes/authorizations/brain_authorization.py index 45ccff45f6fc..e150cb55c5b1 100644 --- a/backend/routes/authorizations/brain_authorization.py +++ b/backend/routes/authorizations/brain_authorization.py @@ -5,6 +5,8 @@ from fastapi import Depends, HTTPException, status from models import UserIdentity from repository.brain import get_brain_for_user +from repository.brain.get_brain_details import get_brain_details + from routes.authorizations.types import RoleEnum @@ -43,6 +45,11 @@ def validate_brain_authorization( return: None """ + brain = get_brain_details(brain_id) + + if brain and brain.status == "public": + return + if required_roles is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/backend/routes/subscription_routes.py b/backend/routes/subscription_routes.py index d43b5d3865da..93e6056a7ab5 100644 --- a/backend/routes/subscription_routes.py +++ b/backend/routes/subscription_routes.py @@ -361,3 +361,45 @@ def update_brain_subscription( update_brain_user_rights(brain_id, user_id, subscription.rights) return {"message": "Brain subscription updated successfully"} + + +@subscription_router.post( + "/brains/{brain_id}/subscribe", + tags=["Subscription"], +) +async def subscribe_to_brain_handler( + brain_id: UUID, current_user: UserIdentity = Depends(get_current_user) +): + """ + Subscribe to a public brain + """ + if not current_user.email: + raise HTTPException(status_code=400, detail="UserIdentity email is not defined") + + brain = get_brain_by_id(brain_id) + + if brain is None: + raise HTTPException(status_code=404, detail="Brain not found") + if brain.status != "public": + raise HTTPException( + status_code=403, + detail="You cannot subscribe to this brain without invitation", + ) + # check if user is already subscribed to brain + user_brain = get_brain_for_user(current_user.id, brain_id) + if user_brain is not None: + raise HTTPException( + status_code=403, + detail="You are already subscribed to this brain", + ) + try: + create_brain_user( + user_id=current_user.id, + brain_id=brain_id, + rights=RoleEnum.Viewer, + is_default_brain=False, + ) + except Exception as e: + raise HTTPException(status_code=400, detail=f"Error adding user to brain: {e}") + + return {"message": "You have successfully subscribed to the brain"} diff --git a/frontend/app/brains-management/[brainId]/components/BrainManagementTabs/BrainManagementTabs.tsx b/frontend/app/brains-management/[brainId]/components/BrainManagementTabs/BrainManagementTabs.tsx index 9f3bc287410d..87b361271488 100644 --- a/frontend/app/brains-management/[brainId]/components/BrainManagementTabs/BrainManagementTabs.tsx +++ b/frontend/app/brains-management/[brainId]/components/BrainManagementTabs/BrainManagementTabs.tsx @@ -1,12 +1,15 @@ +/* eslint-disable max-lines */ import { Content, List, Root } from "@radix-ui/react-tabs"; import { useTranslation } from "react-i18next"; import Button from "@/lib/components/ui/Button"; +import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext"; import { BrainTabTrigger, KnowledgeTab, PeopleTab } from "./components"; import ConfirmationDeleteModal from "./components/Modals/ConfirmationDeleteModal"; import { SettingsTab } from "./components/SettingsTab/SettingsTab"; import { useBrainManagementTabs } from "./hooks/useBrainManagementTabs"; +import { isUserBrainOwner } from "./utils/isUserBrainOwner"; export const BrainManagementTabs = (): JSX.Element => { const { t } = useTranslation(["translation", "config", "delete_brain"]); @@ -19,13 +22,21 @@ export const BrainManagementTabs = (): JSX.Element => { setIsDeleteModalOpen, brain, } = useBrainManagementTabs(); - - const isPubliclyAccessible = brain?.status === "public"; + const { allBrains } = useBrainContext(); if (brainId === undefined) { return
; } + const isCurrentUserBrainOwner = isUserBrainOwner({ + brainId, + userAccessibleBrains: allBrains, + }); + + const isPublicBrain = brain?.status === "public"; + + const hasEditRights = !isPublicBrain || isCurrentUserBrainOwner; + return ( { value="settings" onChange={setSelectedTab} /> - {!isPubliclyAccessible && ( + {hasEditRights && ( <> {
- {isPubliclyAccessible && ( + {isPublicBrain && ( {t("brain:public_brain_label")} @@ -80,7 +91,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => { isLoading={isSettingAsDefault} onClick={() => void setAsDefaultBrainHandler()} type="button" - disabled={isPubliclyAccessible} + disabled={!hasEditRights} > {t("setDefaultBrain", { ns: "brain" })} @@ -93,7 +104,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => { placeholder={t("brainDescriptionPlaceholder", { ns: "brain" })} autoComplete="off" className="flex-1 m-3" - disabled={isPubliclyAccessible} + disabled={!hasEditRights} {...register("description")} /> @@ -102,7 +113,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => { placeholder={t("openAiKeyPlaceholder", { ns: "config" })} autoComplete="off" className="flex-1" - disabled={isPubliclyAccessible} + disabled={!hasEditRights} {...register("openAiKey")} />
@@ -111,7 +122,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {