mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-14 17:03:29 +03:00
Feat/shareable brains open link authenticated (#676)
* ♻️ use Single Responsibility Principle on brains_subscription * ✨ new brain subscription endpoints for invited user * 📝 add documentation to endpoints * 🎨 add base_frontend_url to send custom url for brain share * ✏️ brains instead of brain in url * ✨ use origin in header for frontend url in subscription email * 🚚 move and remove unused code * ✨ new subscription API for BE endpoints in frontend * ✨ new addBrain to add a shared brain in frontend * 🥚 new hook for brain invitations * ✨ new page for brain invitation * ✨ change frontend url to copy for brain subscription * ✏️ call RBAC with wrapper function * ✏️ last typos
This commit is contained in:
parent
f8fce33191
commit
0b091bd8c9
@ -10,10 +10,9 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class BrainSubscription(BaseModel):
|
class BrainSubscription(BaseModel):
|
||||||
brain_id: Optional[UUID] = None
|
brain_id: UUID
|
||||||
inviter_email: Optional[str]
|
email: str
|
||||||
email: Optional[str]
|
rights: str = "Viewer"
|
||||||
rights: Optional[str]
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
arbitrary_types_allowed = True
|
arbitrary_types_allowed = True
|
||||||
@ -65,36 +64,4 @@ class BrainSubscription(BaseModel):
|
|||||||
else:
|
else:
|
||||||
response = self.create_subscription_invitation()
|
response = self.create_subscription_invitation()
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get_brain_url(self) -> str:
|
|
||||||
"""Generates the brain URL based on the brain_id."""
|
|
||||||
base_url = "https://www.quivr.app/chat"
|
|
||||||
return f"{base_url}?brain_subscription_invitation={self.brain_id}"
|
|
||||||
|
|
||||||
def resend_invitation_email(self):
|
|
||||||
brains_settings = BrainSettings() # pyright: ignore reportPrivateUsage=none
|
|
||||||
resend.api_key = brains_settings.resend_api_key
|
|
||||||
|
|
||||||
brain_url = self.get_brain_url()
|
|
||||||
|
|
||||||
html_body = f"""
|
|
||||||
<p>This brain has been shared with you by {self.inviter_email}.</p>
|
|
||||||
<p><a href='{brain_url}'>Click here</a> to access your brain.</p>
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
r = resend.Emails.send(
|
|
||||||
{
|
|
||||||
"from": brains_settings.resend_email_address,
|
|
||||||
"to": self.email,
|
|
||||||
"subject": "Quivr - Brain Shared With You",
|
|
||||||
"html": html_body,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
print("Resend response", r)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error sending email: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
return r
|
|
@ -0,0 +1,8 @@
|
|||||||
|
import os
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
|
||||||
|
def get_brain_url(origin: str, brain_id: UUID) -> str:
|
||||||
|
"""Generates the brain URL based on the brain_id."""
|
||||||
|
|
||||||
|
return f"{origin}/invitation/{brain_id}"
|
@ -0,0 +1,37 @@
|
|||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
import resend
|
||||||
|
from logger import get_logger
|
||||||
|
from models.brains_subscription_invitations import BrainSubscription
|
||||||
|
from models.settings import BrainSettings
|
||||||
|
from repository.brain_subscription.get_brain_url import get_brain_url
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def resend_invitation_email(brain_subscription: BrainSubscription, inviter_email: str, origin: str = "https://www.quivr.app"):
|
||||||
|
brains_settings = BrainSettings() # pyright: ignore reportPrivateUsage=none
|
||||||
|
resend.api_key = brains_settings.resend_api_key
|
||||||
|
|
||||||
|
brain_url = get_brain_url(origin, brain_subscription.brain_id)
|
||||||
|
|
||||||
|
html_body = f"""
|
||||||
|
<p>This brain has been shared with you by {inviter_email}.</p>
|
||||||
|
<p><a href='{brain_url}'>Click here</a> to access your brain.</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = resend.Emails.send({
|
||||||
|
"from": "onboarding@resend.dev",
|
||||||
|
"to": brain_subscription.email,
|
||||||
|
"subject": "Quivr - Brain Shared With You",
|
||||||
|
"html": html_body
|
||||||
|
})
|
||||||
|
logger.info('Resend response', r)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error sending email: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
return r
|
@ -0,0 +1,78 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from logger import get_logger
|
||||||
|
from models.brains_subscription_invitations import BrainSubscription
|
||||||
|
from models.settings import CommonsDep, common_dependencies
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionInvitationService:
|
||||||
|
def __init__(self, commons: Optional[CommonsDep] = None):
|
||||||
|
self.commons = common_dependencies()
|
||||||
|
|
||||||
|
def create_subscription_invitation(self, brain_subscription: BrainSubscription):
|
||||||
|
logger.info("Creating subscription invitation")
|
||||||
|
response = (
|
||||||
|
self.commons["supabase"]
|
||||||
|
.table("brain_subscription_invitations")
|
||||||
|
.insert({"brain_id": str(brain_subscription.brain_id), "email": brain_subscription.email, "rights": brain_subscription.rights})
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
|
||||||
|
def update_subscription_invitation(self, brain_subscription: BrainSubscription):
|
||||||
|
logger.info('Updating subscription invitation')
|
||||||
|
response = (
|
||||||
|
self.commons["supabase"]
|
||||||
|
.table("brain_subscription_invitations")
|
||||||
|
.update({"rights": brain_subscription.rights})
|
||||||
|
.eq("brain_id", str(brain_subscription.brain_id))
|
||||||
|
.eq("email", brain_subscription.email)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
|
||||||
|
def create_or_update_subscription_invitation(self, brain_subscription: BrainSubscription):
|
||||||
|
response = self.commons["supabase"].table("brain_subscription_invitations").select("*").eq("brain_id", str(brain_subscription.brain_id)).eq("email", brain_subscription.email).execute()
|
||||||
|
|
||||||
|
if response.data:
|
||||||
|
response = self.update_subscription_invitation(brain_subscription)
|
||||||
|
else:
|
||||||
|
response = self.create_subscription_invitation(brain_subscription)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def check_invitation(self, brain_subscription: BrainSubscription):
|
||||||
|
response = self.commons["supabase"].table("brain_subscription_invitations").select("*").eq("brain_id", str(brain_subscription.brain_id)).eq("email", brain_subscription.email).execute()
|
||||||
|
return response.data != []
|
||||||
|
|
||||||
|
def fetch_invitation(self, subscription: BrainSubscription):
|
||||||
|
logger.info("Fetching subscription invitation")
|
||||||
|
response = (
|
||||||
|
self.commons["supabase"]
|
||||||
|
.table("brain_subscription_invitations")
|
||||||
|
.select("*")
|
||||||
|
.eq("brain_id", str(subscription.brain_id))
|
||||||
|
.eq("email", subscription.email)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
if response.data:
|
||||||
|
return response.data[0] # return the first matching invitation
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def remove_invitation(self, subscription: BrainSubscription):
|
||||||
|
logger.info(f"Removing subscription invitation for email {subscription.email} and brain {subscription.brain_id}")
|
||||||
|
response = (
|
||||||
|
self.commons["supabase"]
|
||||||
|
.table("brain_subscription_invitations")
|
||||||
|
.delete()
|
||||||
|
.eq("brain_id", str(subscription.brain_id))
|
||||||
|
.eq("email", subscription.email)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
logger.info(f"Removed subscription invitation for email {subscription.email} and brain {subscription.brain_id}")
|
||||||
|
logger.info(response)
|
||||||
|
return response.data
|
@ -3,14 +3,10 @@ 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 (
|
from models.brains import (Brain, get_default_user_brain,
|
||||||
Brain,
|
get_default_user_brain_or_create_new)
|
||||||
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__)
|
||||||
|
@ -6,9 +6,7 @@ from models.brains import Brain
|
|||||||
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 (
|
from routes.authorizations.brain_authorization import (
|
||||||
has_brain_authorization,
|
has_brain_authorization, validate_brain_authorization)
|
||||||
validate_brain_authorization,
|
|
||||||
)
|
|
||||||
|
|
||||||
explore_router = APIRouter()
|
explore_router = APIRouter()
|
||||||
|
|
||||||
|
0
backend/core/routes/headers/__init_.py
Normal file
0
backend/core/routes/headers/__init_.py
Normal file
7
backend/core/routes/headers/get_origin_header.py
Normal file
7
backend/core/routes/headers/get_origin_header.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Header
|
||||||
|
|
||||||
|
|
||||||
|
def get_origin_header(origin: Optional[str] = Header(None)):
|
||||||
|
return origin
|
@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
from typing import List
|
from typing import List
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
@ -6,32 +7,42 @@ 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 \
|
||||||
|
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 routes.authorizations.brain_authorization import (
|
from routes.headers.get_origin_header import get_origin_header
|
||||||
has_brain_authorization,
|
|
||||||
)
|
|
||||||
|
|
||||||
subscription_router = APIRouter()
|
subscription_router = APIRouter()
|
||||||
|
subscription_service = SubscriptionInvitationService()
|
||||||
|
|
||||||
|
|
||||||
@subscription_router.post("/brain/{brain_id}/subscription")
|
@subscription_router.post(
|
||||||
async def invite_user_to_brain(
|
"/brains/{brain_id}/subscription",
|
||||||
brain_id: UUID, users: List[dict], current_user: User = Depends(get_current_user)
|
dependencies=[
|
||||||
):
|
Depends(
|
||||||
# TODO: Ensure the current user has permissions to invite users to this brain
|
AuthBearer(),
|
||||||
|
),
|
||||||
|
Depends(has_brain_authorization),
|
||||||
|
Depends(get_origin_header),
|
||||||
|
],
|
||||||
|
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)):
|
||||||
|
"""
|
||||||
|
Invite multiple users to a brain by their emails. This function creates
|
||||||
|
or updates a brain subscription invitation for each user and sends an
|
||||||
|
invitation email to each user.
|
||||||
|
"""
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
subscription = BrainSubscription(
|
subscription = BrainSubscription(brain_id=brain_id, email=user['email'], rights=user['rights'])
|
||||||
brain_id=brain_id,
|
|
||||||
email=user["email"],
|
|
||||||
rights=user["rights"],
|
|
||||||
inviter_email=current_user.email or "Quivr",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subscription.create_or_update_subscription_invitation()
|
subscription_service.create_or_update_subscription_invitation(subscription)
|
||||||
subscription.resend_invitation_email()
|
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}")
|
||||||
|
|
||||||
@ -39,7 +50,7 @@ async def invite_user_to_brain(
|
|||||||
|
|
||||||
|
|
||||||
@subscription_router.get(
|
@subscription_router.get(
|
||||||
"/brain/{brain_id}/users",
|
"/brains/{brain_id}/users",
|
||||||
dependencies=[Depends(AuthBearer()), Depends(has_brain_authorization())],
|
dependencies=[Depends(AuthBearer()), Depends(has_brain_authorization())],
|
||||||
)
|
)
|
||||||
def get_brain_users(
|
def get_brain_users(
|
||||||
@ -66,7 +77,7 @@ def get_brain_users(
|
|||||||
|
|
||||||
|
|
||||||
@subscription_router.delete(
|
@subscription_router.delete(
|
||||||
"/brain/{brain_id}/subscription",
|
"/brains/{brain_id}/subscription",
|
||||||
)
|
)
|
||||||
async def remove_user_subscription(
|
async def remove_user_subscription(
|
||||||
brain_id: UUID, current_user: User = Depends(get_current_user)
|
brain_id: UUID, current_user: User = Depends(get_current_user)
|
||||||
@ -101,3 +112,91 @@ async def remove_user_subscription(
|
|||||||
brain.delete_user_from_brain(current_user.id)
|
brain.delete_user_from_brain(current_user.id)
|
||||||
|
|
||||||
return {"message": f"Subscription removed successfully from brain {brain_id}"}
|
return {"message": f"Subscription removed successfully from brain {brain_id}"}
|
||||||
|
|
||||||
|
|
||||||
|
@subscription_router.get(
|
||||||
|
"/brains/{brain_id}/subscription",
|
||||||
|
dependencies=[Depends(AuthBearer())],
|
||||||
|
tags=["BrainSubscription"],
|
||||||
|
)
|
||||||
|
def get_user_invitation(brain_id: UUID, current_user: User = Depends(get_current_user)):
|
||||||
|
"""
|
||||||
|
Get an invitation to a brain for a user. This function checks if the user
|
||||||
|
has been invited to the brain and returns the invitation status.
|
||||||
|
"""
|
||||||
|
if not current_user.email:
|
||||||
|
raise HTTPException(status_code=400, detail="User email is not defined")
|
||||||
|
|
||||||
|
subscription = BrainSubscription(brain_id=brain_id, email=current_user.email)
|
||||||
|
|
||||||
|
has_invitation = subscription_service.check_invitation(subscription)
|
||||||
|
return {"hasInvitation": has_invitation}
|
||||||
|
|
||||||
|
|
||||||
|
@subscription_router.post(
|
||||||
|
"/brains/{brain_id}/subscription/accept",
|
||||||
|
tags=["Brain"],
|
||||||
|
)
|
||||||
|
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
|
||||||
|
invitation from the subscription invitations and adds the user to the
|
||||||
|
brain users.
|
||||||
|
"""
|
||||||
|
if not current_user.email:
|
||||||
|
raise HTTPException(status_code=400, detail="User email is not defined")
|
||||||
|
|
||||||
|
subscription = BrainSubscription(brain_id=brain_id, email=current_user.email)
|
||||||
|
|
||||||
|
try:
|
||||||
|
invitation = subscription_service.fetch_invitation(subscription)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Error fetching invitation: {e}")
|
||||||
|
|
||||||
|
if not invitation:
|
||||||
|
raise HTTPException(status_code=404, detail="Invitation not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
brain = Brain(id=brain_id)
|
||||||
|
brain.create_brain_user(
|
||||||
|
user_id=current_user.id, rights=invitation['rights'], default_brain=False
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Error adding user to brain: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
subscription_service.remove_invitation(subscription)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Error removing invitation: {e}")
|
||||||
|
|
||||||
|
return {"message": "Invitation accepted successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@subscription_router.post(
|
||||||
|
"/brains/{brain_id}/subscription/decline",
|
||||||
|
tags=["Brain"],
|
||||||
|
)
|
||||||
|
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
|
||||||
|
invitation from the subscription invitations.
|
||||||
|
"""
|
||||||
|
if not current_user.email:
|
||||||
|
raise HTTPException(status_code=400, detail="User email is not defined")
|
||||||
|
|
||||||
|
subscription = BrainSubscription(brain_id=brain_id, email=current_user.email)
|
||||||
|
|
||||||
|
try:
|
||||||
|
invitation = subscription_service.fetch_invitation(subscription)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Error fetching invitation: {e}")
|
||||||
|
|
||||||
|
if not invitation:
|
||||||
|
raise HTTPException(status_code=404, detail="Invitation not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
subscription_service.remove_invitation(subscription)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Error removing invitation: {e}")
|
||||||
|
|
||||||
|
return {"message": "Invitation declined successfully"}
|
||||||
|
@ -118,7 +118,7 @@ def test_delete_all_brains(client, api_key):
|
|||||||
|
|
||||||
# Send a DELETE request to delete the specific brain
|
# Send a DELETE request to delete the specific brain
|
||||||
delete_response = client.delete(
|
delete_response = client.delete(
|
||||||
f"/brain/{brain_id}/subscription",
|
f"/brains/{brain_id}/subscription",
|
||||||
headers={"Authorization": "Bearer " + api_key},
|
headers={"Authorization": "Bearer " + api_key},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
142
frontend/app/invitation/[brainId]/hooks/useInvitation.ts
Normal file
142
frontend/app/invitation/[brainId]/hooks/useInvitation.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
"use client";
|
||||||
|
import { UUID } from "crypto";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useSubscriptionApi } from "@/lib/api/subscription/useSubscriptionApi";
|
||||||
|
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
|
import { useToast } from "@/lib/hooks";
|
||||||
|
|
||||||
|
interface UseInvitationReturn {
|
||||||
|
brainId: UUID | undefined;
|
||||||
|
handleAccept: () => Promise<void>;
|
||||||
|
handleDecline: () => Promise<void>;
|
||||||
|
isValidInvitation: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInvitation = (): UseInvitationReturn => {
|
||||||
|
const params = useParams();
|
||||||
|
const brainId = params?.brainId as UUID | undefined;
|
||||||
|
const { publish } = useToast();
|
||||||
|
if (brainId === undefined) {
|
||||||
|
throw new Error("Brain ID is undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isValidInvitation, setIsValidInvitation] = useState(false);
|
||||||
|
const { acceptInvitation, declineInvitation } = useSubscriptionApi();
|
||||||
|
const checkValidInvitation = useCallback(
|
||||||
|
useSubscriptionApi().checkValidInvitation,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { addBrain, setActiveBrain } = useBrainContext();
|
||||||
|
const router = useRouter();
|
||||||
|
// Check invitation on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const checkInvitationValidity = async () => {
|
||||||
|
try {
|
||||||
|
console.log("Checking invitation validity...");
|
||||||
|
const validInvitation = await checkValidInvitation(brainId);
|
||||||
|
setIsValidInvitation(validInvitation);
|
||||||
|
console.log("validInvitation", validInvitation);
|
||||||
|
if (!validInvitation) {
|
||||||
|
publish({
|
||||||
|
variant: "warning",
|
||||||
|
text: "This invitation is not valid.",
|
||||||
|
});
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking invitation validity:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkInvitationValidity().catch((error) => {
|
||||||
|
console.error("Error checking invitation validity:", error);
|
||||||
|
});
|
||||||
|
}, [brainId, checkValidInvitation]);
|
||||||
|
|
||||||
|
const handleAccept = async () => {
|
||||||
|
// API call to accept the invitation
|
||||||
|
// After success, redirect user to a specific page -> chat page
|
||||||
|
try {
|
||||||
|
const response = await acceptInvitation(brainId);
|
||||||
|
console.log(response.message);
|
||||||
|
|
||||||
|
await addBrain(brainId);
|
||||||
|
setActiveBrain({ id: brainId, name: "BrainName" });
|
||||||
|
|
||||||
|
//set brainId as active brain
|
||||||
|
publish({
|
||||||
|
variant: "success",
|
||||||
|
text: JSON.stringify(response.message),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// @ts-ignore Error is of unknown type
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
if (error.response.data.detail !== undefined) {
|
||||||
|
publish({
|
||||||
|
variant: "danger",
|
||||||
|
// @ts-ignore Error is of unknown type
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||||
|
text: error.response.data.detail,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("Error calling the API:", error);
|
||||||
|
publish({
|
||||||
|
variant: "danger",
|
||||||
|
text: "An unknown error occurred while accepting the invitaiton",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
void router.push("/chat");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDecline = async () => {
|
||||||
|
// API call to accept the invitation
|
||||||
|
// After success, redirect user to a specific page -> home page
|
||||||
|
try {
|
||||||
|
const response = await declineInvitation(brainId);
|
||||||
|
console.log(response.message);
|
||||||
|
|
||||||
|
publish({
|
||||||
|
variant: "success",
|
||||||
|
text: JSON.stringify(response.message),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// @ts-ignore Error is of unknown type
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
if (error.response.data.detail !== undefined) {
|
||||||
|
publish({
|
||||||
|
variant: "danger",
|
||||||
|
// @ts-ignore Error is of unknown type
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||||
|
text: error.response.data.detail,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("Error calling the API:", error);
|
||||||
|
publish({
|
||||||
|
variant: "danger",
|
||||||
|
text: "An unknown error occurred while declining the invitaiton",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
void router.push("/upload");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
brainId,
|
||||||
|
handleAccept,
|
||||||
|
handleDecline,
|
||||||
|
isValidInvitation,
|
||||||
|
isLoading,
|
||||||
|
};
|
||||||
|
};
|
52
frontend/app/invitation/[brainId]/page.tsx
Normal file
52
frontend/app/invitation/[brainId]/page.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Button from "@/lib/components/ui/Button";
|
||||||
|
import PageHeading from "@/lib/components/ui/PageHeading";
|
||||||
|
import Spinner from "@/lib/components/ui/Spinner";
|
||||||
|
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||||
|
import { redirectToLogin } from "@/lib/router/redirectToLogin";
|
||||||
|
|
||||||
|
import { useInvitation } from "./hooks/useInvitation";
|
||||||
|
|
||||||
|
const InvitationPage = (): JSX.Element => {
|
||||||
|
const { handleAccept, handleDecline, isLoading } = useInvitation();
|
||||||
|
const { session } = useSupabase();
|
||||||
|
// Show the loader while invitation validity is being checked
|
||||||
|
if (isLoading) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Modify this to fetch the brain name from the database
|
||||||
|
const brain = { name: "TestBrain" };
|
||||||
|
|
||||||
|
if (session?.user === undefined) {
|
||||||
|
redirectToLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="pt-10">
|
||||||
|
<PageHeading
|
||||||
|
title={`Welcome to ${brain.name}!`}
|
||||||
|
subtitle="You have been exclusively invited to join this brain and start exploring. Do you accept this exciting journey?"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-5 mt-5">
|
||||||
|
<Button
|
||||||
|
onClick={() => void handleAccept()}
|
||||||
|
variant={"secondary"}
|
||||||
|
className="py-3"
|
||||||
|
>
|
||||||
|
Yes, count me in!
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => void handleDecline()}
|
||||||
|
variant={"danger"}
|
||||||
|
className="py-3"
|
||||||
|
>
|
||||||
|
No, thank you.
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InvitationPage;
|
@ -70,7 +70,7 @@ describe("useBrainApi", () => {
|
|||||||
await deleteBrain(id);
|
await deleteBrain(id);
|
||||||
|
|
||||||
expect(axiosDeleteMock).toHaveBeenCalledTimes(1);
|
expect(axiosDeleteMock).toHaveBeenCalledTimes(1);
|
||||||
expect(axiosDeleteMock).toHaveBeenCalledWith(`/brain/${id}/subscription`);
|
expect(axiosDeleteMock).toHaveBeenCalledWith(`/brains/${id}/subscription`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call getDefaultBrain with the correct parameters", async () => {
|
it("should call getDefaultBrain with the correct parameters", async () => {
|
||||||
@ -127,7 +127,7 @@ describe("useBrainApi", () => {
|
|||||||
|
|
||||||
expect(axiosPostMock).toHaveBeenCalledTimes(1);
|
expect(axiosPostMock).toHaveBeenCalledTimes(1);
|
||||||
expect(axiosPostMock).toHaveBeenCalledWith(
|
expect(axiosPostMock).toHaveBeenCalledWith(
|
||||||
`/brain/${id}/subscription`,
|
`/brains/${id}/subscription`,
|
||||||
subscriptions
|
subscriptions
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -142,6 +142,6 @@ describe("useBrainApi", () => {
|
|||||||
await getBrainUsers(id);
|
await getBrainUsers(id);
|
||||||
|
|
||||||
expect(axiosGetMock).toHaveBeenCalledTimes(1);
|
expect(axiosGetMock).toHaveBeenCalledTimes(1);
|
||||||
expect(axiosGetMock).toHaveBeenCalledWith(`/brain/${id}/users`);
|
expect(axiosGetMock).toHaveBeenCalledWith(`/brains/${id}/users`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -40,7 +40,7 @@ export const deleteBrain = async (
|
|||||||
brainId: string,
|
brainId: string,
|
||||||
axiosInstance: AxiosInstance
|
axiosInstance: AxiosInstance
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await axiosInstance.delete(`/brain/${brainId}/subscription`);
|
await axiosInstance.delete(`/brains/${brainId}/subscription`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDefaultBrain = async (
|
export const getDefaultBrain = async (
|
||||||
@ -68,13 +68,13 @@ export const addBrainSubscriptions = async (
|
|||||||
subscriptions: Subscription[],
|
subscriptions: Subscription[],
|
||||||
axiosInstance: AxiosInstance
|
axiosInstance: AxiosInstance
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await axiosInstance.post(`/brain/${brainId}/subscription`, subscriptions);
|
await axiosInstance.post(`/brains/${brainId}/subscription`, subscriptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBrainUsers = async (
|
export const getBrainUsers = async (
|
||||||
brainId: string,
|
brainId: string,
|
||||||
axiosInstance: AxiosInstance
|
axiosInstance: AxiosInstance
|
||||||
): Promise<Subscription[]> => {
|
): Promise<Subscription[]> => {
|
||||||
return (await axiosInstance.get<Subscription[]>(`/brain/${brainId}/users`))
|
return (await axiosInstance.get<Subscription[]>(`/brains/${brainId}/users`))
|
||||||
.data;
|
.data;
|
||||||
};
|
};
|
||||||
|
43
frontend/lib/api/subscription/subscription.ts
Normal file
43
frontend/lib/api/subscription/subscription.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { AxiosInstance } from "axios";
|
||||||
|
import { UUID } from "crypto";
|
||||||
|
|
||||||
|
export const acceptInvitation = async (
|
||||||
|
brainId: UUID,
|
||||||
|
axiosInstance: AxiosInstance
|
||||||
|
): Promise<{ message: string }> => {
|
||||||
|
const acceptedInvitation = (
|
||||||
|
await axiosInstance.post<{ message: string }>(
|
||||||
|
`/brains/${brainId}/subscription/accept`
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
console.log("acceptedInvitation", acceptedInvitation);
|
||||||
|
|
||||||
|
return acceptedInvitation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const declineInvitation = async (
|
||||||
|
brainId: UUID,
|
||||||
|
axiosInstance: AxiosInstance
|
||||||
|
): Promise<{ message: string }> => {
|
||||||
|
const deletedInvitation = (
|
||||||
|
await axiosInstance.post<{ message: string }>(
|
||||||
|
`/brains/${brainId}/subscription/decline`
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
return deletedInvitation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkValidInvitation = async (
|
||||||
|
brainId: UUID,
|
||||||
|
axiosInstance: AxiosInstance
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const answer = await axiosInstance.get<{ hasInvitation: boolean }>(
|
||||||
|
`/brains/${brainId}/subscription`
|
||||||
|
);
|
||||||
|
const toto = answer.data["hasInvitation"];
|
||||||
|
console.log(answer);
|
||||||
|
|
||||||
|
return toto;
|
||||||
|
};
|
23
frontend/lib/api/subscription/useSubscriptionApi.ts
Normal file
23
frontend/lib/api/subscription/useSubscriptionApi.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { UUID } from "crypto";
|
||||||
|
|
||||||
|
import { useAxios } from "@/lib/hooks";
|
||||||
|
|
||||||
|
import {
|
||||||
|
acceptInvitation,
|
||||||
|
checkValidInvitation,
|
||||||
|
declineInvitation,
|
||||||
|
} from "./subscription";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
export const useSubscriptionApi = () => {
|
||||||
|
const { axiosInstance } = useAxios();
|
||||||
|
|
||||||
|
return {
|
||||||
|
acceptInvitation: async (brainId: UUID) =>
|
||||||
|
acceptInvitation(brainId, axiosInstance),
|
||||||
|
declineInvitation: async (brainId: UUID) =>
|
||||||
|
declineInvitation(brainId, axiosInstance),
|
||||||
|
checkValidInvitation: async (brainId: UUID) =>
|
||||||
|
checkValidInvitation(brainId, axiosInstance),
|
||||||
|
};
|
||||||
|
};
|
@ -19,7 +19,7 @@ export const useShareBrain = (brainId: string) => {
|
|||||||
>([generateBrainAssignation()]);
|
>([generateBrainAssignation()]);
|
||||||
|
|
||||||
const baseUrl = window.location.origin;
|
const baseUrl = window.location.origin;
|
||||||
const brainShareLink = `${baseUrl}/brain_subscription_invitation=${brainId}`;
|
const brainShareLink = `${baseUrl}/invitation/${brainId}`;
|
||||||
|
|
||||||
const { publish } = useToast();
|
const { publish } = useToast();
|
||||||
const { addBrainSubscriptions, getBrainUsers } = useBrainApi();
|
const { addBrainSubscriptions, getBrainUsers } = useBrainApi();
|
||||||
|
@ -18,7 +18,7 @@ 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 } =
|
const { createBrain, deleteBrain, getBrains, getDefaultBrain, getBrain } =
|
||||||
useBrainApi();
|
useBrainApi();
|
||||||
|
|
||||||
const [allBrains, setAllBrains] = useState<Brain[]>([]);
|
const [allBrains, setAllBrains] = useState<Brain[]>([]);
|
||||||
@ -45,6 +45,28 @@ 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));
|
||||||
@ -102,6 +124,7 @@ export const useBrainProvider = () => {
|
|||||||
allBrains,
|
allBrains,
|
||||||
createBrain: createBrainHandler,
|
createBrain: createBrainHandler,
|
||||||
deleteBrain: deleteBrainHandler,
|
deleteBrain: deleteBrainHandler,
|
||||||
|
addBrain,
|
||||||
setActiveBrain,
|
setActiveBrain,
|
||||||
fetchAllBrains,
|
fetchAllBrains,
|
||||||
setDefaultBrain,
|
setDefaultBrain,
|
||||||
|
Loading…
Reference in New Issue
Block a user