From c561fe3af4ab9af5c4ffe7a63b6d83d862947d6a Mon Sep 17 00:00:00 2001 From: Ilya Kreymer Date: Fri, 14 Jan 2022 22:53:02 -0800 Subject: [PATCH] Support Invite Info APIs (#82) * backend: support exposing info about a particular invite, fixes part of #35 new apis are: - GET /users/invite/{token}?email={email} - no auth needed, get invite to new user - GET /users/me/invite/{token} - with auth, to get invite to join an archive for an existing user * get archive.name as well if invite is adding to an archive * first camelCase typo --- backend/archives.py | 6 +++--- backend/invites.py | 36 +++++++++++++++--------------------- backend/users.py | 42 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/backend/archives.py b/backend/archives.py index 37560c95..a2078d87 100644 --- a/backend/archives.py +++ b/backend/archives.py @@ -158,10 +158,10 @@ class ArchiveOps: return [Archive.from_dict(res) for res in results] async def get_archive_for_user_by_id( - self, uid: str, user: User, role: UserRole = UserRole.VIEWER + self, aid: str, user: User, role: UserRole = UserRole.VIEWER ): """Get an archive for user by unique id""" - query = {f"users.{user.id}": {"$gte": role.value}, "_id": uid} + query = {f"users.{user.id}": {"$gte": role.value}, "_id": aid} res = await self.archives.find_one(query) return Archive.from_dict(res) @@ -184,7 +184,7 @@ class ArchiveOps: async def handle_new_user_invite(self, invite_token: str, user: User): """Handle invite from a new user""" - new_user_invite = await self.invites.get_valid_invite(invite_token, user) + new_user_invite = await self.invites.get_valid_invite(invite_token, user.email) await self.add_user_by_invite(new_user_invite, user) await self.invites.remove_invite(invite_token) return True diff --git a/backend/invites.py b/backend/invites.py index 0a62ed2a..69913b3e 100644 --- a/backend/invites.py +++ b/backend/invites.py @@ -22,12 +22,14 @@ class UserRole(IntEnum): # ============================================================================ -class InvitePending(BaseModel): - """Pending Request to join""" +class InvitePending(BaseMongoModel): + """An invite for a new user, with an email and invite token as id""" created: datetime + inviterEmail: str aid: Optional[str] role: Optional[UserRole] = UserRole.VIEWER + email: Optional[str] # ============================================================================ @@ -44,13 +46,6 @@ class InviteToArchiveRequest(InviteRequest): role: UserRole -# ============================================================================ -class NewUserInvite(InvitePending, BaseMongoModel): - """An invite for a new user, with an email and invite token as id""" - - email: str - - # ============================================================================ class InviteOps: """ invite users (optionally to an archive), send emails and delete invites """ @@ -61,8 +56,7 @@ class InviteOps: async def add_new_user_invite( self, - new_user_invite: NewUserInvite, - inviter_email: str, + new_user_invite: InvitePending, archive_name: Optional[str], headers: Optional[dict], ): @@ -78,23 +72,21 @@ class InviteOps: self.email.send_new_user_invite( new_user_invite.email, - inviter_email, + new_user_invite.inviterEmail, archive_name, new_user_invite.id, headers, ) - async def get_valid_invite(self, invite_token: str, user): + async def get_valid_invite(self, invite_token: str, email): """ Retrieve a valid invite data from db, or throw if invalid""" invite_data = await self.invites.find_one({"_id": invite_token}) if not invite_data: - print("NO DATA", flush=True) raise HTTPException(status_code=400, detail="Invalid Invite Code") - new_user_invite = NewUserInvite.from_dict(invite_data) - print(new_user_invite, flush=True) + new_user_invite = InvitePending.from_dict(invite_data) - if user.email != new_user_invite.email: + if email != new_user_invite.email: raise HTTPException(status_code=400, detail="Invalid Invite Code") return new_user_invite @@ -133,19 +125,19 @@ class InviteOps: archive_name = archive.name invite_pending = InvitePending( + id=invite_code, aid=aid, created=datetime.utcnow(), role=invite.role if hasattr(invite, "role") else None, + email=invite.email, + inviterEmail=user.email ) other_user = await user_manager.user_db.get_by_email(invite.email) if not other_user: await self.add_new_user_invite( - NewUserInvite( - id=invite_code, email=invite.email, **invite_pending.dict() - ), - user.email, + invite_pending, archive_name, headers, ) @@ -162,6 +154,8 @@ class InviteOps: status_code=400, detail="User already a member of this archive." ) + # no need to store our own email as adding invite to user + invite_pending.email = None other_user.invites[invite_code] = invite_pending await user_manager.user_db.update(other_user) diff --git a/backend/users.py b/backend/users.py index de6e2e80..f14dbe13 100644 --- a/backend/users.py +++ b/backend/users.py @@ -124,7 +124,7 @@ class UserManager(BaseUserManager[UserCreate, UserDB]): raise HTTPException(status_code=400, detail="Invite Token Required") if user.inviteToken and not await self.invites.get_valid_invite( - user.inviteToken, user + user.inviteToken, user.email ): raise HTTPException(status_code=400, detail="Invalid Invite Token") @@ -153,7 +153,13 @@ class UserManager(BaseUserManager[UserCreate, UserDB]): try: res = await self.create( - UserCreate(email=email, password=password, is_superuser=True, newArchive=False, is_verified=True) + UserCreate( + email=email, + password=password, + is_superuser=True, + newArchive=False, + is_verified=True, + ) ) print(f"Super user {email} created", flush=True) print(res, flush=True) @@ -207,7 +213,6 @@ class UserManager(BaseUserManager[UserCreate, UserDB]): user.email, token, request and request.headers ) - ###pylint: disable=no-self-use, unused-argument async def on_after_request_verify( self, user: UserDB, token: str, request: Optional[Request] = None ): @@ -215,6 +220,19 @@ class UserManager(BaseUserManager[UserCreate, UserDB]): self.email.send_user_validation(user.email, token, request and request.headers) + async def format_invite(self, invite): + """ format an InvitePending to return via api, resolve name of inviter """ + inviter = await self.get_by_email(invite.inviterEmail) + result = invite.serialize() + result["inviterName"] = inviter.name + if invite.aid: + archive = await self.archive_ops.get_archive_for_user_by_id(invite.aid, inviter) + result["archiveName"] = archive.name + + return result + + + # ============================================================================ def init_user_manager(mdb, emailsender, invites): @@ -299,6 +317,24 @@ def init_users_api(app, user_manager): return {"invited": "new_user"} + @users_router.get("/invite/{token}", tags=["invites"]) + async def get_invite_info(token: str, email: str): + invite = await user_manager.invites.get_valid_invite(token, email) + return await user_manager.format_invite(invite) + + @users_router.get("/me/invite/{token}", tags=["invites"]) + async def get_existing_user_invite_info(token: str, + user: User = Depends(current_active_user)): + + try: + invite = user.invites[token] + except: + # pylint: disable=raise-missing-from + raise HTTPException(status_code=400, detail="Invalid Invite Code") + + return await user_manager.format_invite(invite) + + app.include_router(users_router, prefix="/users", tags=["users"]) asyncio.create_task(user_manager.create_super_user())