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
This commit is contained in:
Ilya Kreymer 2022-01-14 22:53:02 -08:00 committed by GitHub
parent b9622df9e6
commit c561fe3af4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 57 additions and 27 deletions

View File

@ -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

View File

@ -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)

View File

@ -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())